/ Reference — WP-Cron

WP-Cron Developer Reference: Functions, Constants & CLI

Every WP-Cron function, constant, and WP-CLI command developers actually search for — with the exact signature, the production gotcha, and the link to the canonical Code Reference.

TL;DR

  • WP-Cron is not a real scheduler — it is a function called wp_cron() that fires on page load and dispatches a non-blocking loopback to wp-cron.php.
  • wp_doing_cron() returns true only inside that loopback request (when DOING_CRON is defined). Use it to guard long-running callbacks.
  • DISABLE_WP_CRON in wp-config.php disables the page-load trigger but does not disable wp-cron.php itself — that is the foundation of every real-cron fix.
/ Definition

What is WP-Cron and how does it actually fire?

WP-Cron is a userland scheduler written in PHP. It is not a Unix cron daemon. It runs entirely inside WordPress and is triggered by HTTP requests to your site — most commonly by visitor page loads, but also by the WordPress admin, the REST API, and explicit calls to spawn_cron().

The flow on every request: WordPress boots, runs the wp_cron() function near the end of wp_loaded, checks the cron option in the database for events whose time field is in the past, and — if any are due — calls spawn_cron() to fire a non-blocking wp_remote_post() loopback request against wp-cron.php. The visitor's response is returned without waiting for the cron run to finish.

The canonical handler is wp-cron.php at the WordPress root. It defines the DOING_CRON constant, loads WordPress, iterates the due events, and runs do_action( $hook, $args ) for each.

  Visitor request
       │
       ▼
  WordPress boot ── wp-includes/default-filters.php
       │
       ├─ DISABLE_WP_CRON defined truthy?  ──► skip, do not spawn
       │
       ▼
  wp_cron() reads cron array (option: cron)
       │
       ├─ no events due           ──► return, end request
       │
       ▼
  spawn_cron()
       │
       ▼
  wp_remote_post( wp-cron.php?doing_wp_cron, blocking=false )
       │
       ▼
  ── visitor request ends ──
       │
       (separate PHP request)
       ▼
  wp-cron.php
       │
       ├─ define( 'DOING_CRON', true )
       ├─ load WordPress
       ├─ for each due event:
       │     do_action( $hook, $args )
       └─ exit

See the official WP-Cron handbook for the canonical description of this lifecycle.

/ wp_doing_cron()

What does wp_doing_cron() return and when do you use it?

It returns true when the current PHP request is the cron loopback — that is, when the DOING_CRON constant has been defined. It returns false during normal visitor requests, admin requests, REST requests, and WP-CLI commands (except for wp cron event run, which sets DOING_CRON manually).

Use it to guard long-running work that should never block a user-facing request. A common pattern: an add_action('my_event', ...) callback that does file processing wraps its body in if ( wp_doing_cron() ) as a defensive check, so if the hook is ever fired in another context the work is skipped instead of stalling the request.

Internally it is a thin wrapper around wp_doing_cron() applies the filter of the same name. The signature is wp_doing_cron(): bool. See the wp_doing_cron() reference.

wp_doing_cron() and spawn_cron() in practice
// Inside any request that wants to trigger overdue cron events. if ( ! wp_doing_cron() && _get_cron_array() ) { spawn_cron(); // fires a non-blocking loopback to /wp-cron.php?doing_wp_cron } // Inside a callback registered with add_action('my_cron_event', ...). function my_cron_callback() { if ( wp_doing_cron() ) { // Safe to do long-running work — no user is waiting on this PHP request. } }
/ spawn_cron()

How does spawn_cron() work under the hood?

It fires a wp_remote_post() request to site_url('/wp-cron.php?doing_wp_cron=<timestamp>') with blocking => false. The current request continues immediately; the loopback request runs in a separate PHP process. This is the mechanism that lets the user-facing response return quickly even when cron has work to do. See the spawn_cron() reference.

Before firing, spawn_cron() writes a doing_cron transient with the same timestamp. This is a one-minute soft lock — a second concurrent visitor will see the transient is set, will not call spawn_cron() again, and will trust that the existing loopback is in progress. The transient prevents a swarm of overlapping cron runs on a busy site.

It is safe to call spawn_cron() from your own code if you want to force-fire overdue events without waiting for the next page load. Wrap it in if ( ! wp_doing_cron() ) to avoid recursive loopbacks.

/ DISABLE_WP_CRON

What does DISABLE_WP_CRON actually disable?

Defined as define( 'DISABLE_WP_CRON', true ); in wp-config.php, it suppresses the page-load trigger only. The check happens inside wp_cron() — if the constant is truthy, the function exits immediately without spawning anything. Visitor requests stop firing cron.

It does not disable wp-cron.php itself. The script remains publicly accessible and still iterates due events when hit directly. This is the entire point: pair DISABLE_WP_CRON with a system crontab entry that hits wp-cron.php every minute, and you get reliable time-based execution decoupled from traffic.

It also does not disable scheduling — wp_schedule_event() still writes to the cron option, plugins still register their schedules, and the events still execute when wp-cron.php runs. The constant only affects when cron runs, not whether it runs.

Common mistake: setting DISABLE_WP_CRON without configuring a real cron. The page-load trigger is gone, no system cron exists, so wp-cron.php never runs and every scheduled event silently stops firing. Always pair them — see the system cron setup guide.

/ WP-CLI

Which WP-CLI commands run a real cron sweep?

wp cron event run --due-now is the workhorse. It runs every event whose time is in the past, in the current PHP process (no loopback), with DOING_CRON defined so wp_doing_cron() behaves correctly inside callbacks. This is what you put in your system crontab.

Use wp cron event run <hook> to fire a single named event on demand — useful during plugin development and when investigating a single misbehaving callback. Use wp cron event list for inventory: every registered event, its next-run time, recurrence interval, and the hook name. wp cron test issues a loopback to wp-cron.php and reports whether the server can reach itself (a common failure on managed hosts with strict outbound firewalls).

WP-CLI cron commands — the daily-driver subset
# List every scheduled event. wp cron event list # Run every due event right now, in this PHP process (no loopback). wp cron event run --due-now # Run one specific hook on demand. wp cron event run my_cron_event # Show registered schedules (hourly, twicedaily, daily, plus custom). wp cron schedule list # Check whether wp-cron.php is reachable from the server itself. wp cron test
/ Real Cron

How do you replace WP-Cron with a real system cron?

Two lines of configuration. First, in wp-config.php above the /* That's all, stop editing! */ comment: define( 'DISABLE_WP_CRON', true );. Second, in the system crontab for the web user (commonly www-data on Debian/Ubuntu):

/etc/cron.d/wordpress — guaranteed one-minute execution
# Runs every minute regardless of site traffic. WP-CLI variant preferred when available. * * * * * www-data /usr/local/bin/wp --path=/var/www/site cron event run --due-now >/dev/null 2>&1 # Curl fallback if WP-CLI is not installed: * * * * * www-data curl -s https://your-site.com/wp-cron.php?doing_wp_cron >/dev/null 2>&1

The WP-CLI form is preferred because it runs in-process, captures errors directly, and avoids the HTTP loopback (which can be blocked, throttled, or rate-limited by upstream proxies). The curl form is the universal fallback and works on shared hosting where you cannot install WP-CLI.

/ Scheduling API

Where do wp_schedule_event and wp_schedule_single_event fit?

wp_schedule_event( $timestamp, $recurrence, $hook, $args, $wp_error ) registers a recurring event. $recurrence must match a registered schedule name (hourly, twicedaily, daily, weekly in core; cron_schedules filter to add custom). It writes to the cron option and is idempotent only when $args match exactly — different args produce different scheduled events.

wp_schedule_single_event( $timestamp, $hook, $args ) registers a one-shot event. It deduplicates within a ten-minute window: scheduling the same hook + args twice within ten minutes produces only one event. Use it as the WordPress-native pattern for "do this work later, off the request path."

Pair both with add_action( $hook, $callback ) in the same execution path that will run cron — typically the same plugin file, so the callback is registered every time WordPress loads.

/ vs Action Scheduler

How is WP-Cron different from Action Scheduler?

CapabilityWP-CronAction Scheduler
cron option (autoloaded)Dedicated DB tables
~100 events before option bloat100k+ actions per day
Single loopback per tickConfigurable concurrent batches
Silent — event just doesn't runPer-action status + retry
No log of past runsStatus > Scheduled Actions UI
Page load or system cronSame — uses WP-Cron underneath

Action Scheduler is built on top of WP-Cron — its queue runner is itself a scheduled cron event. Replacing WP-Cron with a system cron makes Action Scheduler reliable; the two work together rather than competing. See the Action Scheduler concurrent batches reference for the throughput tuning that depends on a reliable cron firing the queue runner.

/ FAQ

Common questions

WP-Cron is a PHP function inside WordPress that pretends to be a scheduler. It fires on page load — when a visitor hits any page, WordPress checks for overdue scheduled events and runs them in a separate background HTTP request.

It is not a real Unix cron daemon. A site with no traffic has no WP-Cron execution.
It returns false. wp_doing_cron() returns true only when the DOING_CRON constant is defined — which happens inside wp-cron.php and inside wp cron event run.

In normal visitor requests, admin pages, REST requests, and most WP-CLI commands it returns false.
No. DISABLE_WP_CRON only disables the page-load trigger. wp_schedule_event() still writes events to the cron option, plugins still register schedules, and the events still execute when wp-cron.php is hit directly or by WP-CLI.

It is purely a when switch, not a whether switch.
It runs every scheduled event whose time is in the past, in the current PHP process, with DOING_CRON defined. No HTTP loopback is fired.

This is the WP-CLI command you put in your system crontab for reliable execution: * * * * * www-data wp --path=/var/www/site cron event run --due-now.
Three common causes:

1. DISABLE_WP_CRON is set without a system cron, so wp-cron.php never runs.
2. The site has near-zero traffic and the page-load trigger never fires.
3. The callback was registered with add_action() in a code path that does not load during the cron request.

Run wp cron event list to confirm the event is scheduled, then wp cron event run <hook_name> to confirm the callback executes.