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:

  1. Low-traffic sites: If nobody visits for hours, scheduled posts do not publish, backup plugins do not run, and WooCommerce jobs queue up.
  2. High-traffic sites: Every page view potentially triggers cron checks, adding unnecessary database load.
  3. Cache interference: Full-page cache can prevent WP-Cron from ever firing.
  4. 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

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:

HookPurpose
wp_scheduled_deleteEmpty trash
wp_version_checkCore update check
wp_update_pluginsPlugin update check
wp_update_themesTheme update check
wp_privacy_delete_old_export_filesGDPR cleanup
woocommerce_cleanup_logsWooCommerce log cleanup
Plugin-specific hooksBackup 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 php and 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.