Welcome to HowToShipIt — practical how-to guides for developers: code, AI tools, and servers, explained step by step.

WordPress Cron Is Fake: How to Disable WP-Cron and Set Up Real Cron (2026)

WordPress has a cron system. It has scheduled events, recurring hooks, “next run” times, and a dashboard that lists them all in neat rows. It looks exactly like a task scheduler, and most site owners treat it like one.

It isn’t one. WP-Cron is not a real scheduler. It only checks whether anything is due when somebody loads a page on your site — and if nobody visits, nothing runs. If you want scheduled posts, backups, update checks, and WooCommerce emails to actually fire on time, you need to disable WP-Cron and replace it with a real server cron job. This guide shows you exactly how, in about ten minutes.

Why WP-Cron Is “Fake” (And Why It Matters)

A real cron daemon lives in the server’s operating system and fires jobs at fixed intervals, traffic or no traffic. WP-Cron works completely differently: every time a visitor loads a page, WordPress calls spawn_cron(), which makes a loopback HTTP request to wp-cron.php to check whether any scheduled task is due. No visitor, no check, no tasks.

That single design decision creates two mirror-image failure modes:

  • Low-traffic sites: scheduled posts sit in “Missed schedule” limbo, backups skip nights, and WooCommerce renewal emails fire hours late — or never — because nothing triggered the check.
  • High-traffic sites: every page load spawns a loopback request that runs the scheduler check, adding database queries and latency to real visitor requests. You are literally paying a performance tax on every pageview to emulate a scheduler.

And the nastiest version: someone reads a guide, adds define('DISABLE_WP_CRON', true); to wp-config.php, and never sets up the replacement. The page-load trigger dies silently, WordPress shows no warning, and the scheduler is completely dead — backups stop, scheduled posts never publish, and nobody finds out for months. This guide is built around doing both halves, in the right order.

What Is Actually Riding on This Scheduler?

People treat WP-Cron as a background detail until it breaks. Here is what depends on it on a typical site:

Depends on WP-Cron What the visitor experiences when it stops
Scheduled posts (publish_future_post) The campaign post that never went live
Automatic backups Nothing — until the day you need a restore and there isn’t one
Update checks (wp_version_check) A known vulnerability sitting unpatched
WooCommerce subscription renewals Recurring revenue silently stops billing
WooCommerce order/abandoned-cart emails Revenue that quietly evaporates
Queued form notification emails The “we’ll be in touch” that never comes — the lead email sits in a queue nothing drains
Security scans and plugin cleanups A hack that goes undetected for weeks

WooCommerce stores deserve special attention: Action Scheduler (WooCommerce’s queue for renewal emails, coupon expirations, and abandoned-cart triggers) depends on WP-Cron to tick. When WP-Cron stalls, WooCommerce → Status → Scheduled Actions fills up with “past due” actions — and the fix is the same one in this guide.

Step 0: See What You Have Before You Touch Anything

Do not change anything yet. First, look at the scheduler’s current state. Install the free WP Crontrol plugin and go to Tools → Cron Events. You will see every scheduled event, its arguments, its recurrence, and its next run time. Red warnings mean events with no callback (usually a deleted plugin’s leftover hook) or events that missed their schedule.

If you have SSH and WP-CLI, the terminal tells you the same story faster:

wp cron event list --next_run_relative
wp cron schedule list
wp cron test

wp cron test answers with a success message when WP-Cron spawning works as expected. If publish_future_post shows a past date, that is your proof: the task is overdue simply because no page load has triggered it yet.

Step 1: Set Up the Real Cron FIRST

This is the step people skip, and it is why doing the steps in this order matters. Create the server-side cron job before you disable WP-Cron’s page-load trigger, so there is never a gap where nothing schedules anything.

Pick one of these three methods, from best to acceptable.

Method A: WP-CLI (best)

If your host has WP-CLI installed, this is the fastest and most reliable option — no HTTP overhead, no SSL handshake, no loopback requests:

*/5 * * * * cd /path/to/wordpress && wp cron event run --due-now >/dev/null 2>&1

Replace /path/to/wordpress with your install’s directory. --due-now runs every task whose scheduled time has passed, in one shot.

Method B: PHP directly (good)

No WP-CLI? Call the PHP interpreter on wp-cron.php directly. It still skips the HTTP round-trip:

*/5 * * * * /usr/bin/php -q /path/to/wordpress/wp-cron.php > /dev/null 2>&1

Check your actual PHP path with which php first — on many hosts it is not /usr/bin/php. Running the wrong PHP major version against your site’s code can fatal; match the PHP version your site actually runs.

Method C: HTTP request (acceptable last resort)

If the host only gives you a panel-based cron tool, hit the endpoint over HTTP:

*/5 * * * * curl -s https://yoursite.com/wp-cron.php?doing_wp_cron >/dev/null 2>&1

This works, but it is the weakest option: it still pays the HTTP overhead, and cache rules, firewalls, or basic-auth setups can silently block the request. If you use it, verify the URL loads from the server itself.

How to add it

  • VPS / SSH: run crontab -e and paste your chosen line. Five minutes is the right default.
  • cPanel / Plesk / host panels: find Cron Jobs under the Advanced section, paste the command (the panel takes the timing separately or accepts the full crontab line), and set the interval to every 5 minutes.
  • Shared hosting with no cron tool: use a free external scheduler like cron-job.org to ping https://yoursite.com/wp-cron.php?doing_wp_cron every 5 minutes. In this setup, leave WP-Cron’s page-load trigger enabled — the external pinger covers low-traffic periods and the visitor trigger remains as a fallback.

Step 2: Disable the Page-Load Trigger

Once the server cron is created and you have waited through one tick, open wp-config.php and add this line above the /* That's all, stop editing! */ comment:

define( 'DISABLE_WP_CRON', true );

Be clear on what this does and doesn’t do. It does not delete your scheduled events or stop WordPress from registering them — it only stops WordPress from spawning wp-cron.php on page loads. Your new server cron is now the only thing running the scheduler, which is exactly what you want: one scheduler, firing on a fixed interval, adding zero overhead to visitor page loads.

Note the order constraint the other way round: if you disabled WP-Cron first and added the cron later, nothing was running in between. Treat the constant and the crontab entry as one deployment change — test the crontab, then add the constant.

Step 3: Prove It Works (Don’t Assume)

A cron line in a panel and a constant in wp-config.php are claims, not results. Verify end to end:

  1. Wait two intervals (about 10 minutes).
  2. In WP Crontrol’s Cron Events screen, confirm “Next Run” times are advancing and overdue events are clearing.
  3. If you run WooCommerce: WooCommerce → Status → Scheduled Actions — the “past due” queue should drain.
  4. Run the acid test: schedule a throwaway draft post for 5 minutes from now, close the tab, and don’t touch the site. If it publishes on its own, the whole chain works — constant, crontab, and WordPress.

Check Tools → Site Health too; it reports overdue scheduled events and failed loopback requests, which is useful on hosts where loopback is broken (one reason Method A or B beats Method C).

Picking the Right Interval

  • Every 5 minutes (*/5 * * * *): the right default for almost every site.
  • Every 1–2 minutes: high-volume WooCommerce stores, or temporarily to drain a large backlog of past-due actions — then drop back to 5.
  • Every 10–15 minutes: fine for small blogs that only schedule posts and update checks.
  • Never below 5 unless you have a reason: each tick processes the full queue, and overlapping ticks on a busy store can collide.

Troubleshooting Quick Reference

Symptom Usual cause Fix
Tasks run twice You forgot DISABLE_WP_CRON Add the constant above “stop editing”
Nothing runs at all Wrong path in the crontab, or php/wp not found Run which php and which wp; use the full paths
HTTP cron never fires Cache, CDN, firewall, or basic auth blocks wp-cron.php Prefer Method A or B; whitelist the endpoint if stuck with C
Huge backlog of past-due actions WP-Cron was stalled for a long time Run every 1 minute temporarily, then return to 5
Multisite confusion Cron pointed at a subsite path Point WP-CLI at the network root; it processes events network-wide
Fatal errors when cron fires Wrong PHP version in the crontab Match the PHP major version the site itself runs, not the system default

When You Should NOT Disable WP-Cron

Two exceptions. First, some managed WordPress hosts already run a real server-side scheduler for you — disabling the visitor trigger yourself would create two schedulers or break theirs. Check your host’s docs before touching wp-config.php. Second, tiny low-stakes sites (a hobby blog with scheduled posts only) can keep the default; the payoff of a real cron is reliability and performance, and a quiet blog with no commerce attached may not need either. Everything else — stores, membership sites, anything that sends time-sensitive email — gets the real cron.

The Takeaway

WP-Cron’s fatal flaw was never a bug; it was the design — a scheduler that only wakes up when someone knocks on the door. The fix is two lines in the right order: a server cron job that ticks every 5 minutes, then DISABLE_WP_CRON to retire the page-load trigger. Do it today, run the throwaway-draft test, and stop wondering whether your scheduled tasks actually ran.

Further Reading & References

Leave a Comment