Type something to search...
How to Replace WP-Cron with a Real Server Cron Job?

How to Replace WP-Cron with a Real Server Cron Job?

A post scheduled for 9:00 AM goes live at 11:40 AM. A WooCommerce subscription renewal runs a day late. A backup plugin swears it runs nightly, but the last archive is from last week. On a busy site, the opposite happens: every page view triggers a background request to wp-cron.php, and under load those requests pile up and eat PHP workers. Both problems have the same cause. WordPress does not have a real scheduler. It has WP-Cron, a scheduler that only runs when someone visits the site.

This article explains how WP-Cron actually works, when it causes problems, and how to replace it with a real server cron job. It covers disabling the built-in trigger, running due events with WP-CLI or a plain HTTP request, using a systemd timer instead of crontab, handling multisite, scheduling your own events correctly, and verifying that everything still runs.

How WP-Cron Works

WP-Cron is not a daemon. Scheduled events are stored in a single option, cron, in the wp_options table. Each event has a timestamp, a hook name, arguments, and optionally a recurrence such as hourly or daily.

On every page load, WordPress calls wp_cron() during init. It checks whether any event is due. If one is, WordPress fires a non-blocking loopback HTTP request to https://example.com/wp-cron.php?doing_wp_cron=<timestamp>. That separate request loads WordPress, takes a lock, and runs every due event by calling do_action() on its hook.

This design has two consequences:

  1. No visitors, no cron. If nobody visits the site between 2:00 AM and 8:00 AM, nothing scheduled in that window runs until the first visit after 8:00 AM.
  2. Visitors pay for cron. Each page load checks the schedule, and due events cost an extra PHP request. On a busy site with many due events, that adds up.
ProblemCause in WP-CronEffect
Missed scheduled postsNo traffic at the scheduled timePosts publish late or show "Missed schedule"
Late emails, renewals, and backupsEvents run only on the next visitBusiness processes drift by hours
Extra load under trafficCron check on every request, loopback to wp-cron.phpWasted PHP workers during peaks
Cron never runsLoopback blocked by firewall, basic auth, or DNSNothing scheduled runs at all
Page caching hides trafficFull-page cache serves visitors without running PHPCron triggers far less often than expected

The last row is easy to miss. If a CDN or full-page cache serves most visitors, PHP rarely runs, so WP-Cron rarely fires, even on a site with plenty of traffic. Caching is still the right choice for speed, as explained in what is the purpose of a caching plugin in WordPress, but it makes a real cron job even more important.

When You Should Replace WP-Cron

Replace WP-Cron with a server cron job if any of these apply:

  • You schedule posts and they sometimes publish late
  • You run WooCommerce, memberships, subscriptions, or newsletters
  • Backups, imports, or syncs must run at a predictable time
  • The site sits behind full-page caching or a CDN
  • Site Health reports "A scheduled event is late" or "A scheduled event has failed"
  • The server blocks loopback requests

On a small brochure site with steady traffic and no time-sensitive tasks, WP-Cron is usually fine. Everywhere else, a real cron job is cheap insurance.

Step 1: Disable the Built-In Trigger

First, tell WordPress to stop spawning cron on page loads. Add this to wp-config.php above the line that says /* That's all, stop editing! */:

// wp-config.php
define( 'DISABLE_WP_CRON', true );

This does not disable scheduled events. Events are still registered, stored, and run. It only stops page views from triggering wp-cron.php. From now on, something else must run due events, or nothing will.

Do not deploy this line on its own. Set up the replacement trigger in the next step first, then disable WP-Cron, so there is no gap.

Step 2: Choose How to Run Due Events

You have two good options. Both are triggered by the system scheduler, usually crontab.

MethodHow it runsBest for
WP-CLI cron event runPHP CLI process on the serverVPS, dedicated servers, managed hosts with SSH and WP-CLI
HTTP request to wp-cron.phpcurl or wget hits the site URLShared hosting without WP-CLI, or external cron services

WP-CLI is the better choice when available. It runs in the PHP CLI, so web server timeouts and PHP-FPM worker limits do not apply, and long tasks such as imports are less likely to be killed halfway through.

Option A: WP-CLI

First check that WP-CLI works for the site and can see the events:

# Terminal
cd /var/www/example.com/public
wp cron event list --fields=hook,next_run_relative,recurrence
wp cron event run --due-now

wp cron event run --due-now runs every event whose time has passed, then exits. That is exactly what a cron job should call.

Now open the crontab for the user that owns the WordPress files. On most servers this is the web server user, or a dedicated site user. Running WP-CLI as root creates files owned by root and breaks uploads and updates later.

# Terminal
sudo crontab -u www-data -e

Add this line to run due events every five minutes:

# crontab for www-data
*/5 * * * * cd /var/www/example.com/public && /usr/local/bin/wp cron event run --due-now --quiet >> /var/log/wp-cron-example.log 2>&1

Notes on this line:

  • Use the full path to wp. Cron runs with a minimal PATH. Find the path with which wp.
  • cd into the WordPress root, or pass --path=/var/www/example.com/public.
  • --quiet suppresses the success line for every event so the log only grows when something goes wrong. Remove it while testing.
  • Every five minutes is a sensible default. Use every minute for sites that rely on precise timing, such as subscription renewals or flash sales.

Preventing Overlapping Runs

If a run takes longer than the interval, the next run starts while the first is still going. WordPress takes its own lock, but it is cheap to add an OS-level lock with flock:

# crontab for www-data
*/5 * * * * cd /var/www/example.com/public && /usr/bin/flock -n /tmp/wp-cron-example.lock /usr/local/bin/wp cron event run --due-now --quiet >> /var/log/wp-cron-example.log 2>&1

flock -n exits immediately if the previous run still holds the lock, so runs never stack up.

Option B: HTTP Request to wp-cron.php

Without WP-CLI, have cron request wp-cron.php directly. This goes through the web server, exactly like WP-Cron's own loopback, but on a fixed schedule instead of on page views:

# crontab
*/5 * * * * curl -fsS --max-time 300 -o /dev/null "https://example.com/wp-cron.php?doing_wp_cron" >> /var/log/wp-cron-example.log 2>&1

Or with wget:

# crontab
*/5 * * * * wget -q -O /dev/null --timeout=300 "https://example.com/wp-cron.php?doing_wp_cron"

On shared hosting, add the same command in your control panel's cron jobs screen, such as cPanel → Cron Jobs. If your host gives you no cron at all, an external monitoring or cron service that requests that URL on a schedule works the same way.

Things to check with the HTTP method:

  • The URL must be reachable from where the request runs. Basic auth on a staging site, a "coming soon" plugin, or a firewall rule can block it.
  • Long tasks hit web timeouts. PHP's max_execution_time, PHP-FPM's request_terminate_timeout, and proxy timeouts all apply. If tasks get killed, switch to WP-CLI.
  • Exclude wp-cron.php from page caching and bot protection, so the request reaches PHP.

Step 3: Use a systemd Timer Instead of Crontab (Optional)

On modern Linux servers, a systemd timer is an alternative to crontab with better logging and built-in overlap protection. Create a service unit:

# /etc/systemd/system/wp-cron-example.service
[Unit]
Description=Run due WordPress cron events for example.com

[Service]
Type=oneshot
User=www-data
WorkingDirectory=/var/www/example.com/public
ExecStart=/usr/local/bin/wp cron event run --due-now

Then a timer that triggers it:

# /etc/systemd/system/wp-cron-example.timer
[Unit]
Description=Run WordPress cron for example.com every 5 minutes

[Timer]
OnBootSec=2min
OnUnitActiveSec=5min
AccuracySec=30s
Persistent=true

[Install]
WantedBy=timers.target

Enable and start it:

# Terminal
sudo systemctl daemon-reload
sudo systemctl enable --now wp-cron-example.timer
systemctl list-timers wp-cron-example.timer
journalctl -u wp-cron-example.service --since "1 hour ago"

A oneshot service never runs twice at the same time, and journalctl keeps the output without extra log files.

Step 4: Handle WordPress Multisite

On a multisite network, each site has its own cron option. A single wp cron event run --due-now only runs events for the main site. Loop over every site instead:

# /usr/local/bin/wp-cron-network.sh
#!/usr/bin/env bash
set -euo pipefail

WP=/usr/local/bin/wp
WP_PATH=/var/www/network.example.com/public

for url in $("$WP" site list --field=url --archived=0 --deleted=0 --spam=0 --path="$WP_PATH"); do
  "$WP" cron event run --due-now --quiet --url="$url" --path="$WP_PATH"
done

Make it executable and call it from crontab:

# Terminal
sudo chmod +x /usr/local/bin/wp-cron-network.sh
sudo crontab -u www-data -e
# crontab for www-data
*/5 * * * * /usr/bin/flock -n /tmp/wp-cron-network.lock /usr/local/bin/wp-cron-network.sh >> /var/log/wp-cron-network.log 2>&1

For very large networks, run sites in parallel or spread them across several schedules so a full pass finishes within the interval.

Step 5: Verify That Events Run

Once the cron job is in place, confirm it works before you rely on it:

# Terminal
wp cron test
wp cron event list --fields=hook,next_run_gmt,next_run_relative,recurrence
wp cron event list --fields=hook,next_run_relative --format=table | grep "ago"
  • wp cron test checks whether WP-Cron's own spawning works. With DISABLE_WP_CRON set, it reports that the constant is set and spawning is disabled. That is expected, because your server cron job now does the spawning. The command is most useful before the switch, to confirm loopback requests work.
  • Events with a next_run_relative value in the past ("5 minutes ago") are overdue. After your cron job runs, they should disappear from that list and reappear with a future time if they recur.

In the admin, Tools → Site Health reports late or failed scheduled events. A clean Site Health screen a day after the switch is a good sign.

To prove the whole chain end to end, schedule a post a few minutes ahead, wait, and confirm it published on time. Do this on a site with no other traffic, such as staging, so a visitor cannot accidentally trigger it.

Scheduling Your Own Events Correctly

A real cron job only runs what is scheduled. If you write plugins or theme code, schedule events in a way that survives deactivation and does not create duplicates.

Registering a Custom Interval

WordPress ships with hourly, twicedaily, daily, and weekly. Add others with the cron_schedules filter:

// wp-content/plugins/wsm-sync/wsm-sync.php
add_filter( 'cron_schedules', function ( array $schedules ): array {
$schedules['every_fifteen_minutes'] = array(
'interval' => 15 * MINUTE_IN_SECONDS,
'display'  => __( 'Every 15 Minutes', 'wsm-sync' ),
);
return $schedules;
} );

Scheduling on Activation and Cleaning Up on Deactivation

// wp-content/plugins/wsm-sync/wsm-sync.php
/**
 * Plugin Name: WSM Inventory Sync
 * Description: Syncs inventory from the warehouse API every 15 minutes.
 * Version: 1.0.0
 */

const WSM_SYNC_HOOK = 'wsm_sync_inventory';

register_activation_hook( __FILE__, function (): void {
if ( ! wp_next_scheduled( WSM_SYNC_HOOK ) ) {
wp_schedule_event( time(), 'every_fifteen_minutes', WSM_SYNC_HOOK );
}
} );

register_deactivation_hook( __FILE__, function (): void {
wp_clear_scheduled_hook( WSM_SYNC_HOOK );
} );

add_action( WSM_SYNC_HOOK, 'wsm_sync_inventory' );

function wsm_sync_inventory(): void {
$response = wp_remote_get(
'https://warehouse.example.com/api/stock',
array( 'timeout' => 20 )
);

if ( is_wp_error( $response ) ) {
error_log( 'Inventory sync failed: ' . $response->get_error_message() );
return;
}

$items = json_decode( wp_remote_retrieve_body( $response ), true );
// Update products from $items here.
}

Key rules:

  • Always guard with wp_next_scheduled() before wp_schedule_event(). Without it, every activation adds another copy of the event.
  • Clear the hook on deactivation with wp_clear_scheduled_hook(), or the event stays in the cron option forever and runs a missing callback.
  • Arguments matter. wp_next_scheduled( 'hook', array( 42 ) ) and wp_next_scheduled( 'hook' ) are different events. Pass the same arguments everywhere.
  • Keep callbacks idempotent. If a run is missed and two are due, or a run is retried, running the task twice should not double-charge a customer or send an email twice.

For one-off tasks, use wp_schedule_single_event( time() + HOUR_IN_SECONDS, 'hook_name' ).

WP-CLI for Managing Events

# Terminal
wp cron schedule list
wp cron event run wsm_sync_inventory
wp cron event schedule wsm_sync_inventory now every_fifteen_minutes
wp cron event delete wsm_sync_inventory

Running one hook by name with wp cron event run <hook> is the fastest way to debug a task without waiting for its schedule.

ALTERNATE_WP_CRON: A Fallback, Not a Fix

Some servers block the loopback request WP-Cron depends on. WordPress has a fallback constant for that case:

// wp-config.php
define( 'ALTERNATE_WP_CRON', true );

With it, WordPress runs cron by redirecting the visitor's request to a URL with ?doing_wp_cron appended, instead of making a loopback request. The visitor's request does the work, and a query string shows up in their address bar. It is a workaround for broken loopbacks, and it still depends on traffic. If you can add a real cron job, do that and leave this constant out.

Common Problems and Fixes

  • Nothing runs after setting DISABLE_WP_CRON. The cron job is not running or is failing silently. Check the log file you redirected output to, run the exact command by hand as the same user, and confirm which wp matches the path in crontab.
  • "Error: This does not seem to be a WordPress installation." The command is not running from the WordPress root. Add cd /path/to/wordpress && or --path=.
  • Files created by cron are owned by root. The crontab belongs to root. Move the job to the site user's crontab with crontab -u www-data -e, and fix ownership with chown -R www-data:www-data wp-content.
  • PHP version mismatch. The CLI uses a different PHP binary than the web server, so code fails only from cron. Check with php -v and point WP-CLI to the right binary with the WP_CLI_PHP environment variable.
  • The curl method returns 401 or 403. The site is behind basic auth, a maintenance plugin, or a security firewall. Allow wp-cron.php, or switch to WP-CLI.
  • Long tasks stop halfway through. Web timeouts kill the HTTP method. Use WP-CLI, which runs without them, and split very large jobs into batches.
  • Duplicate events pile up. Code calls wp_schedule_event() on every request without a wp_next_scheduled() check. Fix the code, then clean up with wp cron event delete <hook> and reschedule once.
  • Multisite sub-sites never run their events. Only the main site is processed. Loop through wp site list as shown above.

WP-Cron FAQ

Only if nothing replaces it. The constant stops page views from triggering cron, but scheduled posts are still stored as events. Once a server cron job runs due events every few minutes, scheduled posts publish on time, usually more reliably than before.

Every five minutes suits most sites. Use every minute for stores with subscriptions, timed sales, or other tasks where a few minutes matter. Running more often than every minute is not possible with crontab and is rarely needed.

WP-CLI is better when you have SSH access. It runs as a command-line PHP process, avoids web server timeouts and worker limits, and does not depend on the site being reachable over HTTP. The curl method is the right fallback on shared hosting without WP-CLI.

It removes the cron check and the occasional loopback request from visitor page loads, which helps busy sites and sites with many scheduled tasks. The bigger benefit is reliability: tasks run on time regardless of traffic or caching.

Some managed WordPress hosts already disable WP-Cron and run it from the server. Check your host's documentation or look for DISABLE_WP_CRON in wp-config.php before adding your own job, so events are not run by two schedulers.

It is a fallback for servers that block the loopback request WP-Cron normally makes. WordPress redirects a visitor request to run cron instead. It still depends on traffic, so a server cron job is the better fix whenever you can add one.

Conclusion

WP-Cron is a clever compromise for hosting environments without a scheduler, but it ties your scheduled tasks to visitor traffic. On sites behind caching, on low-traffic sites, and on stores that depend on renewals and emails, that means late or missed work. Replacing it takes a few minutes: set DISABLE_WP_CRON, add a crontab entry or systemd timer that runs wp cron event run --due-now every few minutes, loop over sites on multisite, and confirm with wp cron event list and Site Health that nothing is overdue.

If you also write your own scheduled tasks, guard them with wp_next_scheduled(), clear them on deactivation, and make them safe to run twice. The scheduler will then do exactly what you expect, at the time you expect it. For more tools to keep a site in shape, see the ultimate guide to WordPress maintenance and how to use WP-CLI to manage WordPress from the command line.

Here are some useful references for going deeper on WordPress cron:

  1. WordPress Developer Resources: Hooking WP-Cron Into the System Task Scheduler — the official guide to replacing the built-in trigger.
  2. WordPress Developer Resources: Scheduling WP Cron Events — wp_schedule_event(), wp_next_scheduled(), and deactivation cleanup.
  3. WordPress Developer Resources: Understanding WP-Cron Scheduling — intervals and the cron_schedules filter.
  4. WP-CLI Commands: wp cron — every subcommand for listing, running, scheduling, and deleting events.
  5. WordPress Developer Resources: Editing wp-config.php — DISABLE_WP_CRON, ALTERNATE_WP_CRON, and related constants.
Tags :
Share :

Related Posts

WordPress optimization with specific recommended approach

WordPress optimization with specific recommended approach

Whether you run a high traffic WordPress installation or a small blog on a low cost shared host, you should optimize WordPress and your server to run

Continue Reading
Creating and Customizing WordPress Child Themes

Creating and Customizing WordPress Child Themes

Creating a child theme in WordPress is a best practice for making modifications to a theme. By using a child theme, you can update the parent theme w

Continue Reading
Understanding the Distinction Categories vs. Tags in WordPress

Understanding the Distinction Categories vs. Tags in WordPress

WordPress, a powerful content management system, offers a plethora of features to organize content effectively. Among these features, categories and

Continue Reading