WordPress has its own scheduler, WP-Cron, for publishing scheduled posts, sending emails, running backups, clearing caches and plugin tasks. It has a design quirk: it only runs when someone visits the site. On every page load, WordPress checks whether tasks are due and, if so, fires a background request to wp-cron.php.
That causes two problems:
- Low-traffic sites: no visitors at 3 a.m. means the 3 a.m. backup doesn’t run, and scheduled posts publish late (“Missed schedule”).
- High-traffic sites: the check runs on every request and can spawn overlapping cron runs, adding load.
On a VPS the fix is simple: disable the page-load trigger and run cron from the system scheduler.
1. Disable the page-load trigger
In wp-config.php, above “That’s all, stop editing!”:
define( 'DISABLE_WP_CRON', true );
Or with WP-CLI:
sudo -u www-data wp config set DISABLE_WP_CRON true --raw
This doesn’t disable scheduled tasks — it only stops visitors from triggering them. You must now run them yourself (step 2), or nothing scheduled will happen.
2a. Run cron with WP-CLI (recommended)
Edit the web user’s crontab:
sudo crontab -u www-data -e
*/5 * * * * cd /var/www/example.com && /usr/local/bin/wp cron event run --due-now --quiet
Every five minutes, WP-CLI runs all due events directly in PHP-CLI — no HTTP request, no web server timeouts, no PHP-FPM worker tied up. Use command -v wp to confirm the path.
Running as www-data ensures files created by tasks (backups, cache files) have the right owner.
2b. Or request wp-cron.php over HTTP
If WP-CLI isn’t available:
*/5 * * * * curl -s -o /dev/null https://example.com/wp-cron.php?doing_wp_cron
This goes through the web server like a visitor would, so PHP-FPM time limits apply. Long tasks may be cut off — WP-CLI is preferable.
2c. Or a systemd timer
For logging in the journal and no overlapping runs:
/etc/systemd/system/wp-cron-example.service:
[Unit]
Description=WP-Cron for example.com
[Service]
Type=oneshot
User=www-data
WorkingDirectory=/var/www/example.com
ExecStart=/usr/local/bin/wp cron event run --due-now --quiet
/etc/systemd/system/wp-cron-example.timer:
[Unit]
Description=Run WP-Cron every 5 minutes
[Timer]
OnBootSec=2min
OnUnitActiveSec=5min
[Install]
WantedBy=timers.target
sudo systemctl daemon-reload
sudo systemctl enable --now wp-cron-example.timer
systemctl list-timers | grep wp-cron
See systemd timers.
Multisite
Each site in a network has its own cron queue. Run them all:
*/5 * * * * cd /var/www/network && for url in $(/usr/local/bin/wp site list --field=url --archived=0 --deleted=0); do /usr/local/bin/wp cron event run --due-now --url="$url" --quiet; done
Inspect and debug
sudo -u www-data wp cron event list # all scheduled events and next run
sudo -u www-data wp cron event list --fields=hook,next_run_relative,recurrence
sudo -u www-data wp cron event run woocommerce_cleanup_sessions # run one now
sudo -u www-data wp cron test # checks that WP-Cron can spawn (when not disabled)
sudo -u www-data wp cron schedule list # available intervals
Signs of trouble:
- Events with
next_runfar in the past → cron isn’t running; check the crontab andgrep CRON /var/log/syslog. - Thousands of events for the same hook → a misbehaving plugin keeps scheduling duplicates; clean up and report it.
- “Missed schedule” on posts → cron didn’t run at publish time.
The WP Crontrol plugin offers the same overview in wp-admin.
How often?
Every 5 minutes suits most sites. Every minute is fine for stores or sites with time-sensitive tasks (scheduled sales, membership expirations). Running more often than tasks are due costs almost nothing with --due-now.
Heavy tasks
Backups, imports and newsletter sends can run for minutes. With WP-CLI there’s no web timeout, but watch memory and make sure runs don’t overlap. Wrapping the command in flock prevents that:
*/5 * * * * cd /var/www/example.com && flock -n /tmp/wpcron-example.lock /usr/local/bin/wp cron event run --due-now --quiet
Related
Real cron is one of the small changes that make WordPress more reliable on a WordPress VPS — something shared hosting often doesn’t allow.