Technote

Servers & infrastructure Advanced

Series WP-CLI automation recipes Part 7 of 8

Moving wp-cron to system cron, and why schedules run late

Scheduled posts that publish late are usually a structural problem, not a code one. WordPress cron rides on incoming requests, so on a quiet site nothing runs at all.

A scheduled post publishes hours late, a backup has not run for days, update checks go stale. You read the code and find nothing wrong. The usual cause is that WordPress cron is not really cron.

Why it is not really cron

Real cron watches a clock and wakes itself. WordPress does something else: on incoming requests it checks whether anything is due and, if so, fires a separate request back at itself to run the work.

Three consequences follow from that design.

Same scheduled work — what wakes it decides whether it is on time

On a quiet site, nothing runs. If a handful of people visit per day, scheduled posts being hours late is correct behaviour rather than a fault.

Adding a page cache makes it worse. Cached responses never reach PHP, so even with visitors the check does not happen. Scheduled work starving the week after a performance improvement is a well-worn path.

Then there are servers with a blocked loopback. Where a machine cannot call its own domain — firewall, hosts file, internal DNS — the check happens but the request that would do the work never arrives. The symptom is not “sometimes”; it is “never”.

Moving it — both steps in one deploy

First, stop running on requests.

define( 'DISABLE_WP_CRON', true );

Stopping here is the worst possible state. Events keep queueing with nothing to run them and the site says nothing about it. Ship both steps together.

Then register the real schedule.

*/5 * * * * cd /var/www/example && /usr/local/bin/wp cron event run --due-now --quiet

Three things to get right: install it for the web user (crontab -u www-data -e), call wp by absolute path because cron’s PATH is not your login shell’s, and pick an interval that matches the precision you need — five minutes bounds scheduled publishing to five minutes of drift, and one minute is available if you need tighter.

Checking afterwards

wp cron event list shows what is queued. Any entry whose next run keeps sliding further into the past is an event that is not being executed. When things are healthy the time moves forward instead.

wp cron event list --fields=hook,next_run_relative,recurrence
wp cron event run --due-now --dry-run

wp cron test checks whether the loopback spawn works. After moving to system cron, failing that test is fine — you no longer depend on the spawn. Not knowing this leads people to diagnose a perfectly good configuration as broken.

Scheduled publishing is decided in GMT

If cron is healthy but publication times are still odd, the remaining suspect is the data. The publish event is scheduled from post_date_gmt. As part five showed, updating a post date without passing the GMT value leaves the old one behind, so the listed date and the actual publication moment disagree.

Your site timezone setting feeds the same calculation. Left at the installation default, displayed dates drift from what you intended.

What you gain by moving

The biggest gain is not punctuality but a record. Redirect the cron entry’s output to a file and you know when things ran and can attach an alert when they fail. Riding on page requests, no such record existed at all.

A step further is monitoring. Rather than a person noticing days later that scheduled work stopped, something like InfraGuard continuous monitoring watches the state so that silent failures stop being silent.

Server-layer configuration and cache placement are covered further in the servers and infrastructure archive.

Next part

One part left. It covers wiring the commands, scripts and checks from this series into a pipeline, so that nobody has to remember to run them.

More on this topic

All technotes

Servers & infrastructure Practical

Page caching belongs on the server layer

The layer that stores finished HTML only pays off in front of PHP. Stack a plugin cache on top of a server cache and you now have two…

Developers 7 min read

₩270,000 · Join the program