WP Cron Jobs What They Are and How to Fix Delays

WP Cron Jobs What They Are and How to Fix Delays

WordPress cron is not running when you expect it to, and the usual cause is that the built-in scheduler only fires when someone visits your site. The fix is to replace that visitor-triggered system with a real server-side cron job, which is more reliable, more precise, and easier to debug. Here is the mechanics of how the built-in system works, why it fails, and exactly what to change.

How the built-in cron actually works

WordPress does not use the operating system’s cron daemon. Instead, it stores a list of scheduled tasks in the wp_options table, under the cron option name. Every time a page is loaded, WordPress checks that option, compares the stored timestamps with the current time, and runs any task whose timestamp has passed. That check happens on every request, but only on requests that reach PHP, so if your site has no visitors, no requests arrive, and no task ever runs.

Even with traffic, the check is not a separate process. It runs inside the request lifecycle, after the main query but before the page is sent to the browser. That is why a scheduled post can appear minutes late during a burst of traffic, or why a backup cron job can stall a page load. The system is designed for convenience, not for precision.

There is also a subtle protocol detail. When WordPress decides a task is due, it fires a non-blocking HTTP request to wp-cron.php using a special header, X-WP-Cron: 1, and then continues serving the page. If that request is slow, or if the server is configured to block loopback requests, the task is simply deferred to the next page load. That is why you see intermittent delays even on busy sites.

Why the default setup fails for real work

The built-in cron is fine for checking for plugin updates or sending a welcome email. It is not fine for anything where timing matters, like a nightly database backup, a scheduled report, or a sync with an external API. The failure modes are predictable. Low traffic means tasks run hours late. High traffic means tasks run concurrently, because each page load can trigger the same overdue task. And if your site uses a page cache, the cache can serve the HTML without ever executing PHP, so the cron check never happens at all.

You can see the problem directly. Look at the cron option in the database with a query like this:

SELECT option_value FROM wp_options WHERE option_name = 'cron';

That returns a serialized array of scheduled events. If you see timestamps in the past, those tasks are overdue. The built-in system will run them on the next request, but that request may be a long time coming.

Disable the built-in cron and use the real one

The standard fix is to disable the HTTP-based trigger and point a server cron at wp-cron.php directly. You do that by adding a single line to wp-config.php:

define('DISABLE_WP_CRON', true);

Place that line above the /* That's all, stop editing! */ comment. Once that constant is defined, WordPress stops checking for due tasks on every page load. It still stores the schedule, but it will never run anything on its own. Then you create a real cron job on the server, typically in crontab or a systemd timer, that calls the PHP file directly.

A typical crontab entry looks like this:

* * * * * php /path/to/your/site/wp-cron.php >/dev/null 2>&1

That runs every minute, every day. The >/dev/null part suppresses output, because wp-cron.php returns nothing useful on success. You may need to adjust the path to the PHP binary, especially if you use a custom PHP version or a shared host. Use which php to find the absolute path, and test the command manually before adding it to the crontab.

What to check when the cron still does not run

If you set that up and tasks are still delayed, the problem is not the scheduler. It is the environment. First, confirm that the cron job is actually executing. Add a temporary line to the crontab that writes to a log file:

* * * * * echo "$(date) cron fired" >> /tmp/wp-cron.log

Wait two minutes, then read the file. If the timestamp updates, the cron daemon is working. If not, your host may be blocking cron, or the crontab syntax may be wrong. Next, check that the PHP command can find the WordPress installation. Run the command manually from the same directory context that cron uses. Cron runs with a minimal environment, so the path to PHP may differ from your interactive shell.

Another common issue is the loopback request. Even with DISABLE_WP_CRON set, some plugins call wp_remote_post() to trigger their own tasks. Those calls can fail if the server cannot reach itself. On a typical VPS, that means checking that localhost resolves and that your firewall allows loopback traffic. On a shared host, it often means the host blocks loopback for security. The symptom is the same: the task appears scheduled, but never runs.

Diagnosing with WP-CLI

If you have shell access, the fastest way to see what is scheduled and what is stuck is WP-CLI. The command wp cron event list shows every scheduled event with its next run time and hook name. You can also run wp cron event run --all to force every due task to execute immediately. That is a great test because it bypasses the HTTP layer entirely and runs the tasks in the PHP process directly. If that works but the server cron does not, the problem is the cron daemon or the PHP binary path, not WordPress.

You can also check the last run time of a specific hook with wp cron event list and compare it to the current time. The output shows the schedule, like twicedaily or a custom interval, and the timestamp. If the timestamp is more than a few minutes in the past, the task is overdue. If it is in the future, the task is simply not due yet.

Best practices for reliable scheduling

Set the server cron interval to one minute, because WordPress schedules tasks in minute granularity. That is the default for most setups, and it matches the built-in system’s behavior. Do not use a five minute interval unless you know your tasks tolerate that delay. For tasks that must run at a specific time, like a daily backup at 2 a.m., write a small wrapper script that checks the current hour and exits early if it is not the right time. That is more robust than relying on the cron daemon’s own time-based syntax, because WordPress may reschedule the event after a failed run.

Also, be aware that disabling the built-in cron breaks any plugin that assumes it runs on page load. Some plugins, especially cache plugins, add their own cron handling. Read the plugin documentation before you disable the system. And always test after the change: create a test event with wp cron event schedule, wait a minute, then verify it ran.

What to do next

Start by adding the DISABLE_WP_CRON constant and setting up a server cron that calls wp-cron.php every minute. Then verify with wp cron event list that your overdue tasks actually execute. If they do not, check the cron log, the PHP path, and the loopback connectivity. Once the server cron is confirmed working, you can forget about visitor-triggered scheduling entirely. Your backups, emails, and updates will run on time, every time, without depending on who happens to visit your site.

Related articles

Subscribe to our newsletter

Get the latest hosting tips, performance insights, and industry news.