Home Blog

, ,

The Illusion of a Schedule: Why WP-Cron Isn’t a Real Cron Job, and How That Trips Up Projects at Any Scale

The Problem WP-Cron looks like an ordinary system cron: you register an event, set an interval, and it “runs on schedule.” That’s an illusion. There is no real background process behind it…

The Problem

WP-Cron looks like an ordinary system cron: you register an event, set an interval, and it “runs on schedule.” That’s an illusion. There is no real background process behind it — WordPress checks the queue of scheduled events on every page load, and if an event’s time has come, it runs right there inside that same HTTP request, invisibly to the visitor. That’s exactly what makes WP-Cron convenient (no server access needed to schedule anything) and treacherous at the same time: it only works when someone visits the site, and it silently doesn’t work when nobody does.

Challenges and Problems

There’s one core problem here, but it shows up differently depending on scale: WP-Cron isn’t a timer — it’s a side effect of traffic. On a site with no visitors, tasks simply don’t run, and nobody finds out until it becomes critical. On a high-traffic site, it’s the opposite — the queue check runs on every single request, and if left uncontrolled, it turns into constant overhead and a source of duplicates. On enterprise infrastructure (multisite, containers that scale down to zero), both problems stack on top of each other, plus you get the added question of which instance, and for which site in the network, an event was even supposed to run.

The three hooks in this category are exactly the points where you can bring this behavior under control: cron_schedules (which intervals even exist), cron_request (how WordPress actually initiates the queue check), and wp_scheduled_delete (what happens during the daily trash cleanup — a typical example of a built-in scheduled task).

Typical Cases

Case 1 — A Regular Site (Low Traffic)

The challenge. A blog or small business site with a handful of visits a day. A backup or cache-cleanup plugin is scheduled for “daily at 3 AM” — but at 3 AM, there’s simply nobody on the site. WordPress only checks the queue when someone visits, so the task runs late — whenever the first visitor shows up in the morning, not at night as expected. The most dangerous part is that this looks like a working solution: the task does eventually run, just not when it was supposed to.

Steps to fix it.

  1. Don’t rely on exact execution timing for time-critical tasks — WP-Cron only guarantees “not before the scheduled time,” never “exactly at the scheduled time.”
  2. Register only the intervals that genuinely match the task’s actual need, via cron_schedules, instead of forcing your logic to fit the built-in hourly/daily options.
  1. For tasks where a delay is unacceptable even on a low-traffic site, disable the pseudo-cron and hook up a real system cron from your hosting that pings wp-cron.php on a schedule:

Case 2 — A High-Traffic Project (Heavy Load)

The challenge. A media site or online store handling hundreds of requests per minute. By default, every single one of those requests triggers a WP-Cron queue check — meaning an extra HTTP call to wp-cron.php on every page load whenever an event is due. Under high concurrency, several requests can see an event as “due” at nearly the same moment and fire it twice — a classic reason a notification or email suddenly gets sent to a user two times in a row.

Steps to fix it.

  1. Disable the automatic pseudo-cron (DISABLE_WP_CRON, as in Case 1) and move the queue check to a separate, time-controlled trigger — so cron overhead no longer piles onto every page visit.
  2. Adjust the spawn request itself through cron_request — typically to raise the timeout for slow jobs, or to add a header that identifies the request as internal so it isn’t counted as regular traffic in your metrics:
  1. Guard against concurrent execution right inside the handler itself — don’t rely on there being only one event; add your own lock via a transient:

Case 3 — Enterprise (Multisite, Complex Infrastructure)

The challenge. A network of dozens of sites on a single install, or a container-based infrastructure where instances scale down to zero when there’s no traffic. WordPress’s pseudo-cron is tied to a specific site in the network — and a low-traffic site in that network might never get its tasks triggered at all, while another site generates all the network’s traffic. In container infrastructure it’s worse: if an instance has gone to sleep from lack of requests, there’s no request available to wake the cron queue at all — tasks stall indefinitely. Separately: by default, wp_scheduled_delete permanently deletes the network’s entire overdue trash in one pass, every day — at enterprise data volumes, that’s a real hit to the database and storage right at the moment it runs.

Steps to fix it.

  1. Remove the dependency on pseudo-cron entirely: DISABLE_WP_CRON at the network level, and an external orchestrator (system cron, a cloud scheduler, a Kubernetes CronJob) as the single source of truth for when and for which site tasks should run — one controlled trigger per site, no duplication.
  2. Use WP-CLI instead of an HTTP request to wp-cron.php wherever server access is available — this removes the dependency on a network round-trip and works even for network sites with zero traffic:
  1. Add your own check inside wp_scheduled_delete before mass deletion — for example, stop the automatic deletion if the trash volume exceeds an expected threshold, and leave a trace in an audit log:
  1. Centralize monitoring: at this scale, a silently skipped task is already an incident, not a minor detail — so you need dedicated alerting for “expected a run, didn’t get one.”

Common Mistakes

Registering an event via wp_schedule_event() with an interval name that was never added through cron_schedules — the event simply never runs, with no error at all. Skipping a wp_next_scheduled() check before scheduling — this leads to duplicate copies of the same event piling up every time a plugin activates. Leaving “orphaned” scheduled events behind after a plugin is deactivated — WordPress keeps trying to call a function that no longer exists, and that quietly ends up in the error log. Assuming that a disabled DISABLE_WP_CRON automatically means “cron doesn’t run” — in reality it only means “doesn’t trigger automatically on a visit,” not “disabled entirely”: without an external trigger, the queue is simply never checked at all. And finally — trusting the default 30-day trash retention without checking whether that matches the project’s actual data-retention requirements.

When It’s Better to Bring In a Specialist

A cron audit becomes especially relevant in two opposite situations: when someone on a high-traffic project notices duplicate emails or notifications and can’t figure out where they’re coming from, or when a task on an enterprise network “just stopped running” with no trace in the logs. In both cases, hunting down the cause yourself by trial and error costs more than a single consultation: a WordPress developer experienced with high-traffic and multisite projects checks the whole chain — interval registration, event scheduling, and how the queue gets spawned — systematically, instead of patching symptoms one at a time. This is exactly the kind of case where WordPress development services pay for themselves right away.

FAQ

No. WP-Cron only guarantees that an event won’t run before its scheduled time — the actual moment it runs depends on when the next visitor arrives. For truly time-critical tasks, you need an external trigger: a system cron or a cloud scheduler.

Deactivating a plugin removes the specific events it scheduled. DISABLE_WP_CRON doesn’t remove any events at all — it only turns off the automatic queue check on page visits. Without an external trigger, the queue then never gets checked at all, so this constant should only be turned on alongside setting up a real cron.

No, this filter only applies to recurring intervals used by wp_schedule_event(). A one-time event runs once at the specified moment and doesn’t need an interval registered.

The most common reason is that the event was scheduled via wp_schedule_event() before WordPress had a chance to read the cron_schedules filter (for example, the hook is attached on a later action), or the interval name differs by even one character between the two places it’s used.

The hook covers the permanent deletion of all overdue trash content — posts, attachments (media files), and comments. For media files, that means deleting the physical files from storage too, not just the database record.

If a project has already seen duplicate scheduled actions, tasks “going missing” with no explanation, or the network is scaling up and there’s a growing risk that one site’s tasks are running at the expense of another site’s traffic — that’s a signal to review the entire cron setup holistically, rather than patching individual symptoms.