WP Webhooks / Blog / Architecture
Article · Architecture

WP-Cron Developer Reference: Functions, Constants & CLI

Complete WP-Cron reference for developers — wp_doing_cron, spawn_cron, DISABLE_WP_CRON, WP-CLI cron commands, with examples and production gotchas.

8 min 2026-05-17
#wordpress#wp#spawn

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
Storagecron option (autoloaded)Dedicated DB tables
Scale ceiling~100 events before option bloat100k+ actions per day
ConcurrencySingle loopback per tickConfigurable concurrent batches
Failure recoverySilent — event just doesn't runPer-action status + retry
ObservabilityNo log of past runsStatus > Scheduled Actions UI
TriggerPage 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 always ask.

Don't see yours? Open an issue on GitHub or check the full reference in the API docs.

What is WP-Cron in plain terms? +
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.
What does wp_doing_cron() return outside of the cron loopback? +
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, and REST requests it returns false.
Does DISABLE_WP_CRON disable wp_schedule_event? +
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.
What does wp cron event run --due-now do? +
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.
Why doesn't my scheduled event ever fire? +
Three common causes: DISABLE_WP_CRON is set without a system cron, so wp-cron.php never runs; the site has near-zero traffic and the page-load trigger never fires; or 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.
Ready

Stop losing webhooks.
Start logging them.

$ wp plugin install flowsystems-webhook-actions --activate