WP-Cron runs when someone visits the site. That is convenient for shared hosting, but it is unreliable for low-traffic sites and wasteful for busy ones. A real cron job gives scheduled tasks a predictable heartbeat.
Why WP-Cron fails silently
WP-Cron does not use the operating system’s cron daemon — it is a virtual cron that fires on HTTP requests. This causes several problems:
- Low-traffic sites: If nobody visits for hours, scheduled posts do not publish, backup plugins do not run, and WooCommerce jobs queue up.
- High-traffic sites: Every page view potentially triggers cron checks, adding unnecessary database load.
- Cache interference: Full-page cache can prevent WP-Cron from ever firing.
- Race conditions: Multiple concurrent visitors can trigger the same cron event multiple times.
Real system cron solves all of these.
Step 1: Disable traffic-triggered WP-Cron
Add to wp-config.php:
define('DISABLE_WP_CRON', true);
This stops WordPress from spawning cron on page views. It does not disable scheduled events — it just removes the unreliable trigger.
Place it above the line that says:
/* That's all, stop editing! Happy publishing. */
Step 2: Add system cron
Using WP-CLI (recommended)
For a single site:
*/5 * * * * cd /var/www/example.com/htdocs && wp cron event run --due-now --quiet
The --due-now flag ensures only events that are due run. The --quiet flag suppresses output.
For multisite
Run per subsite if needed:
*/5 * * * * cd /var/www/example.com/htdocs && wp site list --field=url | xargs -I{} wp cron event run --due-now --quiet --url={}
Via HTTP (fallback if WP-CLI unavailable)
*/5 * * * * curl -s https://example.com/wp-cron.php?doing_wp_cron >/dev/null 2>&1
WP-CLI is preferred because it avoids HTTP, TLS termination, redirects, page cache, and PHP-FPM overhead. The HTTP approach also needs DISABLE_WP_CRON to be true; otherwise requests can trigger cron twice.
Step 3: Run as the correct user
The cron job must run as the web server user, not root:
# Check who owns the files
ls -la /var/www/example.com/htdocs/wp-config.php
# Add cron as the correct user
sudo -u www-data crontab -e
If WP-CLI is not in the PATH for that user, use the full path:
*/5 * * * * cd /var/www/example.com/htdocs && /usr/local/bin/wp cron event run --due-now --quiet
Step 4: Check and manage events
List all scheduled events
wp cron event list
wp cron event list --fields=hook,next_run_relative,recurrence
Check for overdue events
wp cron event list --status=due
Events overdue by hours or days indicate the old WP-Cron was not firing.
Run overdue events manually
wp cron event run --due-now
Delete stuck or unwanted events
wp cron event delete hook_name
Common scheduled tasks
WP-Cron handles many tasks you might not realise:
| Hook | Purpose |
|---|---|
wp_scheduled_delete | Empty trash |
wp_version_check | Core update check |
wp_update_plugins | Plugin update check |
wp_update_themes | Theme update check |
wp_privacy_delete_old_export_files | GDPR cleanup |
woocommerce_cleanup_logs | WooCommerce log cleanup |
| Plugin-specific hooks | Backup schedules, email queues, cache preloading |
If cron stops, these quietly drift. Scheduled posts do not publish. WooCommerce order status emails do not send. Backup plugins silently skip their jobs.
Monitoring: detect silent failure
Silent cron failure is common and painful. Set up monitoring:
Heartbeat approach
Add a custom cron event that updates a timestamp:
// In a custom plugin or theme functions.php
add_action('crondaily_heartbeat', function () {
update_option('crondaily_heartbeat_last', time());
});
if (!wp_next_scheduled('crondaily_heartbeat')) {
wp_schedule_event(time(), 'every_five_minutes', 'crondaily_heartbeat');
}
Then monitor the option:
# Alert if heartbeat is older than 15 minutes
LAST=$(wp option get crondaily_heartbeat_last 2>/dev/null || echo 0)
NOW=$(date +%s)
DIFF=$(( (NOW - LAST) / 60 ))
if [ "$DIFF" -gt 15 ]; then
echo "CRON DOWN: Last heartbeat was $DIFF minutes ago on example.com"
fi
External monitoring
Services like UptimeRobot, HetrixTools, or Cronitor can monitor cron job execution with heartbeat URLs. If the cron job hits a monitoring URL on each successful run, you get alerted independently of server-side monitoring.
Troubleshooting
WP-CLI cannot find WordPress
wp --path=/var/www/example.com/htdocs cron event list
If WP-CLI still fails:
- Check the user running the cron has read access to
wp-config.php - Confirm
DB_HOST,DB_USER, etc. are defined - Check PHP binary:
which phpand confirm it is the right version
Memory limits
Large cron jobs (backup plugins, WooCommerce exports) may hit memory limits:
# Run with extra memory
php -d memory_limit=512M /usr/local/bin/wp cron event run --due-now
Plugin-specific cron failures
Some plugins hook into cron with heavy operations. If backups run via cron and fail, check:
- Disk space:
df -h - PHP max_execution_time
- External API connectivity (S3, Google Drive, SFTP)
Multiple WP-CLI versions
If WP-CLI is installed both globally and via Composer, you may get version mismatches. Use which wp and wp --version to confirm.
Per-site isolation
If you run multiple WordPress sites on one server, give each its own cron line:
# Site 1
*/5 * * * * cd /var/www/site1.com/htdocs && wp cron event run --due-now --quiet
# Site 2
*/5 * * * * cd /var/www/site2.com/htdocs && wp cron event run --due-now --quiet
# Site 3
*/5 * * * * cd /var/www/site3.com/htdocs && wp cron event run --due-now --quiet
Run them staggered by a minute if all sites share the same database server, to avoid simultaneous heavy queries.
Reverting to WP-Cron
If you need to revert, remove the DISABLE_WP_CRON line from wp-config.php and comment out the system cron job. WP-Cron will resume on the next page view. The scheduled events themselves do not change — only the trigger mechanism.