PHP code on screen showing define() and require_once statements, relevant to wp-config.php configuration

wp-cron Not Working? Real Fixes, Not Plugin Guesses

You scheduled a post for 9 AM. It is now 2 PM and the post is still sitting in “Scheduled.” Your backup plugin missed its window. The SEO plugin’s broken-link scanner has not run in days. Nothing threw an error. Nothing logged a warning. You checked the settings, re-saved the post, and nothing changed. If your wp-cron is not working, this is what it looks like: silent failure across every scheduled task on your site, with no error message to point you at the cause. The problem is not your plugins. It is the scheduling mechanism underneath them.

Here is the short version: WordPress does not have a real cron daemon. It has a PHP script that runs when someone visits your site. No visitors means no scheduled tasks. The fix is to disable the visitor-triggered cron and wire up a real system cron that fires on a clock. This takes about 15 minutes, costs nothing on most hosts, and eliminates an entire category of silent failures. Let’s walk through it.

Why wp-cron Depends on Site Traffic (and Always Has)

Server rack with green status LEDs in a data center, representing the server infrastructure that wp-cron depends on

WP-Cron is a pseudo-scheduler written in PHP. On every page load that reaches PHP (not served from cache), WordPress checks a list of scheduled tasks stored in the wp_options database table under the cron key. If any task’s scheduled time has passed, WordPress fires a non-blocking HTTP request back to wp-cron.php on the same server. That request executes the due tasks and exits. The visitor’s page load completes normally, and they never see a delay.

The critical detail: no visitors, no cron. A post scheduled for 9 AM on a Sunday will not publish until the first person lands on your site after 9 AM. On a low-traffic blog, that could be hours. On a staging site, it could be days. The WordPress Plugin Handbook confirms this is by design: WP-Cron does not run continuously, and disabling it requires you to schedule the task externally.

This traffic-dependent design breaks in four specific scenarios:

Low-Traffic Sites

Staging environments, B2B portals, internal knowledge bases, brochure sites. All of them can sit idle for hours or days. While they do, scheduled posts do not publish, plugin update checks do not run, transient cleanups skip, and on WooCommerce sites the Action Scheduler queue starts piling up. This is the most common cause of wp-cron not working: there simply are not enough page loads to trigger it.

Full-Page Caching

If your site uses Varnish, nginx fastcgi_cache, LSCache, or Cloudflare Cache Reserve, requests served from cache never reach PHP. WordPress never bootstraps, spawn_cron() never fires, and scheduled tasks silently miss their window. A site can serve thousands of cached pages an hour while every scheduled job quietly fails. This is particularly insidious because your site appears fast and healthy from the visitor’s perspective. The failure is invisible.

Blocked Loopback Requests

WP-Cron works by sending an HTTP request from your server to itself. If that loopback is blocked by BasicAuth covering the entire site, a security plugin that blocks “server to itself” requests, Cloudflare bot rules dropping origin-IP traffic, or a TLS handshake failure inside the host’s network, the cron request never arrives. The visitor’s page loads fine. The cron silently does not run. This is one of the hardest failures to diagnose because everything on your end appears correct.

High-Traffic Sites

The other side of the same coin. On a busy site, every uncached page load triggers a schedule check: a database read and a CPU hit attached to visitor requests, hundreds or thousands of times per hour. A one-minute soft lock (the doing_cron transient) prevents overlapping runs, but the per-request overhead is real. Most sites will not notice. The ones that do tend to swap WP-Cron for a real system cron to remove the per-page-load check entirely.

How to Tell When wp-cron Is Not Working

The symptom is always the same: scheduled tasks miss their time. Scheduled posts sit in “Scheduled” past their publish time. Plugin tasks show “last run” timestamps that are hours or days stale. No error message appears in the admin dashboard because wp-cron does not fail loudly. It fails by not running at all. Here is how to confirm the cause.

WP Crontrol (No SSH Required)

WP Crontrol is a free plugin from the WordPress.org directory. Install it, go to Tools > Cron Events, and you get a list of every scheduled task on your site with its next run time, recurrence, and callback. Events past their scheduled time show a red “Missed” label. If you see missed events, your wp-cron is not firing on page loads.

WP Crontrol also surfaces spawning errors directly. If you see “There was a problem spawning a call to the WP-Cron system,” the plugin identifies the specific HTTP error. The WP Crontrol documentation maps each error to its cause: cURL error 28 (timeout), cURL error 7 (connection refused), HTTP 401 or 403 (blocked by auth or firewall), HTTP 500 (server error). You can also manually run any event from the WP Crontrol interface to confirm the task itself works. If a manually triggered event succeeds but the event keeps missing its schedule, the problem is the trigger, not the task.

WP-CLI (If You Have Shell Access)

If you have SSH access, WP-CLI gives you the most direct diagnostic. Run these commands from your WordPress root directory:

wp cron event list shows all scheduled hooks, their next run time, and whether any are overdue.

wp cron test tests whether the cron system can spawn HTTP requests. If this returns an error, you have a loopback or connectivity problem. Do not run this command if you have already set DISABLE_WP_CRON to true. It checks whether visitor-triggered spawning works, and it will error because you have intentionally disabled it.

wp cron event run --due-now manually fires all overdue events. Use this to clear a backlog after fixing the underlying issue.

Reading the Error Signs

Different failures leave different fingerprints:

  • Missed schedules with no error message: WP-Cron is not being triggered. Check for full-page caching or low traffic.
  • “Problem spawning” with cURL error 28: The loopback request is timing out. Check PHP timeout settings and server load.
  • “Problem spawning” with HTTP 401 or 403: Something is blocking the server’s request to itself. Check BasicAuth, security plugins, and firewall rules.
  • “Problem spawning” with HTTP 500: A PHP error is occurring during the cron run. Check wp-content/debug.log with WP_DEBUG_LOG enabled.
  • “Problem spawning” with HTTP 404: The wp-cron.php file is missing from your WordPress root. Reinstall WordPress core from Dashboard > Updates.

DISABLE_WP_CRON: What It Does and What It Does Not Do

Laptop screen showing WordPress or PHP code, representing editing wp-config.php to set the DISABLE_WP_CRON constant

DISABLE_WP_CRON is a constant you define in wp-config.php. It stops WordPress from checking for due cron tasks on every page load. That is all it does. It does not disable wp-cron.php itself. The file still sits in your WordPress root, still answers HTTP requests, and still runs whatever events are due when called directly. This is the entire point: you pair the constant with a system cron job that hits wp-cron.php on a fixed schedule, and you get reliable, time-based execution decoupled from site traffic.

To enable it, add this line to wp-config.php, above the /* That's all, stop editing! */ comment:

define( 'DISABLE_WP_CRON', true );

The placement matters. If this line lands below the require_once( ABSPATH . 'wp-settings.php' ); call, WordPress never sees the constant. Both the page-load trigger and your system cron will run, causing duplicate executions. Put it near the top of the file, alongside your other define() statements.

The most common mistake: setting this constant without configuring a real system cron. The page-load trigger is gone, no external trigger exists, so wp-cron.php never runs and every scheduled event silently stops. Scheduled posts never publish. Plugin update checks never run. Backups never fire. If you set DISABLE_WP_CRON, you must complete the next step. There is no middle ground.

Wiring a Real System Cron

Multiple terminal windows with green command-line output on a dark monitor, representing SSH and system cron configuration

Three paths, depending on your hosting environment. All three accomplish the same thing: they trigger wp-cron.php on a fixed schedule so scheduled tasks run on time regardless of traffic. Pick the one that matches your access level.

Path A: cPanel (Shared Hosting, No SSH)

If your host uses cPanel, you have a built-in cron job interface. Go to cPanel > Cron Jobs, under the Advanced section. Click “Add New Cron Job.” For the schedule, select “Every 5 minutes” (*/5 * * * *) for a content site, or “Every minute” (* * * * *) for WooCommerce or any site with time-sensitive tasks.

For the command, enter:

curl -sS --max-time 30 -o /dev/null "https://yourdomain.com/wp-cron.php?doing_wp_cron=1"

Replace yourdomain.com with your actual domain. The --max-time 30 flag caps the request at 30 seconds so a stuck cron run cannot stack up new requests. The -sS flag makes curl silent on success but still prints errors, so failures show up in your cron email. The -o /dev/null discards the response body. If your host does not have curl, wget works too: wget -q -O /dev/null "https://yourdomain.com/wp-cron.php?doing_wp_cron" > /dev/null 2>&1

Path B: SSH + WP-CLI (VPS, Dedicated, Cloud)

If you have SSH access and WP-CLI installed, this is the most reliable option. WP-CLI runs cron events directly in PHP without an HTTP roundtrip, which eliminates the loopback failure mode entirely. Open the crontab for the user that owns the WordPress files (usually www-data on Debian/Ubuntu):

sudo crontab -u www-data -e

Add this line to run due events every minute:

* * * * * /usr/local/bin/wp cron event run --due-now --path=/var/www/yourdomain.com --quiet > /dev/null 2>&1

Replace /var/www/yourdomain.com with your actual WordPress path, and /usr/local/bin/wp with the absolute path to WP-CLI on your server. The --due-now flag runs only events whose time has arrived, not every event in the queue. The --quiet flag suppresses output unless something fails. For a content site where every-minute execution is unnecessary, change * * * * * to */5 * * * * for every five minutes.

After adding the entry, verify that WordPress sees the constant:

sudo -u www-data /usr/local/bin/wp --path=/var/www/yourdomain.com eval 'var_dump( DISABLE_WP_CRON );'

This should print bool(true). If it throws a fatal error about an undefined constant, your define() line landed below the require for wp-settings.php and is not being executed. Move it higher in the file.

Path C: External Cron Service (No Server Access)

If you are on managed WordPress hosting with no SSH, no cPanel cron interface, and no WP-CLI access, use an external service that pings your wp-cron.php URL on a schedule. You create an account, add your wp-cron.php URL as a job, set the interval, and the service sends an HTTP request to that URL on your behalf.

cron-job.org is free, has been running for over 15 years, and supports unlimited jobs down to once per minute. It captures responses and provides a history API. The limitations: responses are only stored for 2 days, only the last 50 executions are saved, and it will not retry failures. There is no formal SLA. For a single WordPress site that just needs wp-cron.php pinged every 5 minutes, the free tier is sufficient.

EasyCron starts at $12 per month and adds retries, longer log retention, email and webhook failure alerts, and one-minute intervals on paid plans. The free tier allows 200 executions per day with a 20-minute minimum interval, which is too limited for most WordPress sites.

Google Cloud Scheduler costs $0.10 per job per month, with the first three jobs free. It is overkill for a single WordPress site but makes sense if you are already on GCP and want cron integrated with your existing infrastructure.

Choosing the Right Interval

The interval you choose determines the maximum delay between a task’s scheduled time and when it actually runs. Every cron execution loads WordPress core, which consumes CPU and memory. On shared hosting, running too frequently can trigger resource limits. On a VPS with WP-CLI, the overhead is minimal because there is no HTTP roundtrip. Here is the honest tradeoff by site type:

Site TypeRecommended IntervalWhy
WooCommerce storeEvery 1-2 minutesOrder processing, stock updates, email notifications need near-real-time execution
Email automationEvery 1-2 minutesTime-sensitive email delivery
Blog with scheduled postsEvery 5 minutesPosts publish within 5 minutes of scheduled time
Membership siteEvery 5 minutesSubscription renewals, drip content, access checks
Low-traffic brochure siteEvery 15 minutesOnly update checks and transient cleanup needed

Do not run cron every minute unless you have a specific reason. WordPress core’s default cron intervals start at hourly, but plugins can register shorter schedules through the cron_schedules filter. Action Scheduler, used by WooCommerce and many other plugins, defaults to running its queue every minute. If you run WooCommerce, use one minute. If you run a content blog, five minutes is fine and produces less log noise. Do not go longer than 15 minutes without a specific reason: scheduled posts can publish 15 minutes late, password-reset email queues back up, and Action Scheduler stops feeling responsive.

Where This Approach Breaks or Is Not Worth It

Switching to a real system cron is not always the right call. Here is when to leave it alone, and when it creates new problems you were not expecting.

Stay with WP-Cron If

You have a medium-traffic site that gets enough visitors for cron to fire reliably within a few minutes of any scheduled task. Scheduled tasks are not mission-critical: a 15-minute delay on a blog post is fine. You do not have SSH access or a cron interface in your hosting panel. The default behavior works for the majority of WordPress sites. Adding a system cron to a site that does not need it is configuration for its own sake, and every configuration you add is one more thing that can break.

Do Not Bother If Your Host Already Handles It

Some managed WordPress hosts already run system cron on their side or offer it through their dashboard. Check your host’s documentation before adding your own. If your host already fires wp-cron.php on a schedule, adding your own cron job creates duplicate runs that waste resources without improving reliability. You would be paying to solve a problem that is already solved.

The Approach Breaks When Loopback Is Blocked

If you are using the cPanel or external service approach (which hits wp-cron.php over HTTPS), the request still needs to reach PHP. Cloudflare bot rules, aggressive firewall configurations, or BasicAuth on the entire site can block these requests. The symptom is a 403 or 503 in your cron logs. The fix is to whitelist wp-cron.php for origin requests in your firewall or security plugin. The WP-CLI approach (Path B) avoids this entirely because it runs in PHP directly, with no HTTP involved. If you cannot fix the loopback issue, switch to WP-CLI if you have shell access, or use an external cron service that hits the public URL from outside your firewall.

It Also Breaks When the Constant Is Placed Wrong

If define( 'DISABLE_WP_CRON', true ); lands below the require_once for wp-settings.php, WordPress never sees it. Both the page-load trigger and your system cron run, causing duplicate cron executions. This wastes resources and can cause race conditions where two processes try to run the same task simultaneously. The doing_cron transient provides a one-minute soft lock that mitigates this, but it is not bulletproof. Put the constant near the top of wp-config.php, alongside your database settings and other defines.

If You Run Automated Content, Cron Is Not Optional

If you are using a content automation tool that schedules posts, the reliability of your publishing schedule is only as good as your cron. ClearPost, our WordPress plugin, drafts SEO-optimized posts and delivers them to your approval queue. Once you approve a post, it can be scheduled for publishing at a specific time. If wp-cron is not firing, that post sits in “Scheduled” until someone visits the site. On a new or low-traffic blog, that could be hours or days.

This is one of the most common integration failures we see. A site owner sets up automated content, schedules posts throughout the week, and wonders why nothing publishes on time. The content pipeline is working. The approval queue is working. The cron trigger is not. Before you debug your content tool, check whether wp-cron is actually firing. The diagnostic takes five minutes with WP Crontrol, and the fix takes ten minutes with a system cron entry.

For a deeper look at the full content automation pipeline and how it connects to WordPress, our guide to content automation for WordPress walks through the architecture from data connection to post-publish monitoring. If you are also troubleshooting REST API authentication issues that affect automated publishing, our fix guide for WordPress application passwords covers every common failure point. And for a broader view of what automated WordPress SEO actually handles versus what still needs a human, our guide on automatic SEO on WordPress separates the solved problems from the ones no plugin can fix.

If your scheduled content is not publishing on time, fix your cron first. Then let ClearPost handle the content pipeline: keyword research, drafting, on-page SEO, internal linking, and WordPress publishing. AI does the heavy lifting, you approve every post before it goes live. No long onboarding, no agency overhead, cancel anytime. Try ClearPost free for 7 days and see what 30 SEO-optimized posts a month looks like compared to the manual cycle you are running now.

Frequently Asked Questions

Common questions about wp-cron failures and system cron setup.

Frequently Asked Questions

Why are my WordPress scheduled posts not publishing on time?

WordPress uses wp-cron, a pseudo-scheduler that only runs when someone visits your site. If your site has low traffic, uses full-page caching, or blocks loopback HTTP requests, wp-cron never fires and scheduled posts sit in Scheduled status until a visitor triggers it. The fix is to disable the page-load trigger with DISABLE_WP_CRON and set up a real system cron job that hits wp-cron.php on a fixed schedule.

What does DISABLE_WP_CRON actually do?

It stops WordPress from checking for due cron tasks on every page load. It does not disable wp-cron.php itself. The file remains accessible and still runs due events when called directly. You pair DISABLE_WP_CRON with a system cron job that triggers wp-cron.php on a fixed schedule. The most common mistake is setting the constant without configuring a system cron, which causes all scheduled tasks to silently stop.

How do I check if wp-cron is working?

Install the free WP Crontrol plugin and go to Tools > Cron Events. Events past their scheduled time show a red Missed label. You can also manually run any event to confirm the task itself works. If you have SSH access, run wp cron test to check whether the cron system can spawn HTTP requests, and wp cron event list to see all scheduled events with their next run times.

Should I disable WP-Cron on every WordPress site?

No. If your site gets enough traffic for cron to fire reliably and scheduled tasks are not mission-critical, the default behavior is fine. You should switch to a real system cron if your site is low-traffic, uses full-page caching, has scheduled tasks that must run on time, or is high-traffic enough that the per-page-load cron check adds measurable overhead. Check whether your host already runs system cron before adding your own.

What cron interval should I use for WordPress?

Every 1-2 minutes for WooCommerce stores or sites with email automation. Every 5 minutes for blogs with scheduled posts or membership sites. Every 15 minutes for low-traffic brochure sites that only need update checks and transient cleanup. Do not run cron every minute unless you have a specific reason, as every execution loads WordPress core and consumes CPU and memory.