Tutorials

WordPress wp-cron; Why It Doesn't Run and How to Replace It with a Real Cron

If your post publishing schedule falls behind or plugins stop working, the problem is wp-cron. A guide to diagnosing and replacing it with a real server cron.

Tutorials

You scheduled a post for 9 AM and it's 11 AM and it still hasn't been published. Or a plugin that's supposed to send a report every hour hasn't sent one in a day. If you open the server log and look for wp-cron.php, you'll see either no records at all, or hundreds of records logged back-to-back within a single second. These two states are two faces of the same problem: WordPress's wp-cron is not a real scheduler.

WordPress has no cron daemon on the server. What's called "wp-cron" is a PHP file that gets called on every page visit. That means your task scheduling is tied to site traffic, not to the system clock. That single sentence explains most of the problems you're chasing.

Why wp-cron doesn't run on a low-traffic site

In the file wp-includes/default-filters.php there's a hook that calls the wp_cron() function every time a page loads. This function checks whether any task is due. If your site gets 50 visits a day and they all arrive at irregular intervals, effectively no one triggers that hook at the right moment. The result is that scheduled posts get published hours late, transactional emails arrive late, and automatic backup plugins sometimes don't run at all on some days.

The diagnostic sign is simple too. Write a small script that reads _get_cron_array() and prints the next_run time of each task. If that time keeps staying in the past and doesn't advance, it means no visit has triggered it. We've explained this differently in the documentation and knowledge base as well, but the test itself is three lines of code.

Why it slows down a high-traffic site

Now consider a high-traffic site. Here the problem is the opposite. Every page visit sends a separate HTTP request to wp-cron.php. That file, with define('DISABLE_WP_CRON', false) enabled, reads the entire cron array on every run, takes a lock, and if any task is due, runs it. On a site with 200 concurrent visits per minute, that means 200 extra requests, each consuming a PHP-FPM connection and a query to the wp_options table.

Look at the numbers. A request to wp-cron.php that has no task to run typically takes between 80 and 150 milliseconds and uses about 15 to 30 megabytes of PHP memory. If your site gets 50 requests per second at peak traffic, that means 50 extra connections per second that do nothing for the user. On a server with pm.max_children = 20, the PHP-FPM pool fills up within seconds and real users queue up. TTFB jumps from 300 milliseconds to over two seconds.

Here's where people go wrong: many think the problem is WordPress itself and go for a caching plugin. Caching speeds up the page, but wp-cron.php requests don't pass through the cache path because they're an independent PHP file. The site looks fast on the homepage, but server load stays high and it throws a 502 every few hours. If you search access.log, the pattern is this: hundreds of lines of POST /wp-cron.php?doing_wp_cron back-to-back, all with a 200 code and high response times.

Replacing it with a real server cron

The standard solution is to turn off the internal wp-cron and create a real cron job at the operating system level. First, add this line to wp-config.php:

define('DISABLE_WP_CRON', true);

Then, with crontab -e under the web server user, add this line:

*/5 * * * * cd /var/www/html && /usr/bin/php wp-cron.php >/dev/null 2>&1

There are two things in this line that are commonly done wrong. First, the cd to the WordPress path is necessary because wp-cron.php reads paths relatively. Second, use the PHP binary of the same version the site runs on; if the server has multiple PHP versions and you call /usr/bin/php while the site runs on PHP 8.2, you may get a Fatal error: Uncaught Error or worse, it silently won't work. Find the correct path with which php in the same environment.

A five-minute interval is right for most sites. If you have a task that needs to run more precisely, reduce the interval to one minute, but keep in mind that each run spins up a full PHP process. On a weak server, 1440 runs a day has its own cost.

A better alternative: WP-CLI

If WP-CLI is installed, write the cron line like this:

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

This version is better because it only runs due tasks, has clean output, and with --quiet doesn't clutter the log. You see the practical difference when you want to manually test a specific task: wp cron event list and wp cron event run woocommerce_scheduled_sales are exactly what you need for debugging.

Comparing the two methods

CriterionDefault wp-cronReal server cron
Dependence on trafficYesNo
Scheduling accuracyIrregular, up to hours of delayAccurate to the cron interval
Cost under high trafficHigh, extra request on every visitFixed, independent of visits
Requires SSH accessNoYes
Suitable forNew site, low traffic, no SSHAny serious site

If you're on shared hosting without SSH access, you have no option but the default wp-cron unless the hosting panel allows defining a Cron Job. In that case, set the interval to 15 minutes and set DISABLE_WP_CRON to true. If you have full server control, there's no reason to keep the internal wp-cron. On every server I set up myself, I turn this off from day one. On Linux hosting with SSH access, this takes two minutes.

Things that break after migration

After turning off wp-cron, check two things. First, some plugins call wp-cron.php directly themselves or rely on the wp_loaded hook; if after the change you see a plugin no longer working, search the PHP log for Undefined index. Second, if the site runs on multiple servers behind a load balancer, define the cron on only one server. Otherwise each server runs the tasks separately and, for example, you send one email four times.

One more note: after migration, check the wp_options table for cron and doing_cron records. If a run crashed midway, the doing_cron lock remains and no task runs anymore. Manually deleting that record solves the problem. I've seen this state on a site that hadn't taken any backup for three weeks and no one had noticed.

To understand what's really happening on the server after these changes, it's worth also setting up website uptime monitoring; short drops caused by the PHP-FPM pool filling up are only visible with continuous monitoring, not by manually opening the site.

Frequently asked questions

Does turning off wp-cron harm the site?

No, provided you've set up the real cron before turning it off. If you set DISABLE_WP_CRON to true and don't create any cron job, no task runs and scheduled posts are never published. The correct order is: first add the cron line, test that it runs, then turn off the internal wp-cron.

How do I know the real cron job is working correctly?

Create a test task that writes a record to the log, or use wp cron event list and see whether the next_run time advances after each run. If the time stays fixed, the cron isn't running. Also check the system cron log with grep CRON /var/log/syslog.

What's a suitable interval for the cron job in minutes?

For most sites, five minutes is enough. If you have a shop plugin that expires pending orders or a queued email sending system, one minute is more reasonable. Less than one minute isn't possible in standard cron and requires second-level crons, which WordPress doesn't support.

What should I do on shared hosting without SSH?

Look for the Cron Jobs section in the hosting panel; most panels offer this. If it's not there, keep the default wp-cron but don't shorten the interval for heavy tasks like backups. Low-traffic sites in this situation should know that their scheduling won't be accurate and shouldn't rely on it.

ServerNet Support

ServerNet engineering & editorial team — specialists in infrastructure, networking and web hosting.

WordPress Hosting
Share:

Comments 0

No comments yet — be the first!

Leave a comment

Related service

WordPress Hosting

A purpose-built WordPress stack on LiteSpeed Enterprise and NVMe — auto-install, secure updates, staging and caching that keeps you on top of Google.