---
title: "WordPress Cron Job: Why WP-Cron Fails (And How to Fix)"
description: "WordPress cron job not running? WP-Cron only fires on page load. Replace it with a real system cron in two lines — full setup with WP-CLI and Action Scheduler."
url: "https://wpwebhooks.org/blog/cron-job-for-wordpress/"
date: "2026-03-30"
---

# WordPress Cron Job: Why WP-Cron Fails (And How to Fix)

TL;DR

-   WP-Cron is not a real scheduler — it runs inside a WordPress page load and only fires when someone visits the site, making it unreliable for background tasks
-   Replace it with a real system cron entry: `*/5 * * * * curl -s https://yoursite.com/wp-cron.php?doing_wp_cron` and disable WP-Cron in `wp-config.php`
-   For complex job management, Action Scheduler (bundled with WooCommerce) provides persistent, queryable background processing built on top of a database queue

/ How It Works

## Why is WP-Cron a **pseudo-scheduler**, not a daemon?

A real cron job is a system-level daemon — `crond` on Linux — that wakes up on a fixed schedule, independent of application traffic. WordPress has no equivalent. What it has is **WP-Cron**: a PHP-based scheduling system that runs inside a normal HTTP request, only when someone visits the site.

The mechanics are straightforward. Every time WordPress bootstraps — on every non-AJAX, non-admin page load — it calls `spawn_cron()`. That function checks whether any scheduled events are due by reading the `cron` option from `wp_options`. If events are overdue, WordPress fires a non-blocking HTTP request to `/wp-cron.php` in the background and continues serving the page. The cron request runs in a separate PHP process, executes due events, and exits. For the full function-by-function reference — `wp_doing_cron()`, `spawn_cron()`, `DISABLE_WP_CRON`, and the `wp cron event run` WP-CLI commands — see the [WP-Cron Developer Reference](https://wpwebhooks.org/blog/wp-cron-developer-reference/).

Scheduled events are stored as a serialised array in `wp_options` under the key `cron`. Each entry maps a Unix timestamp (the next run time) to an array of hook names, and each hook name to its schedule interval and arguments. You can inspect this directly:

WP-CLI — inspect scheduled events

```
# List all scheduled events with next run time and interval
wp cron event list

# Show all registered schedules (hourly, twicedaily, daily, plus any custom ones)
wp cron schedule list

# Inspect the raw _cron option
wp option get cron
```

You can also check via PHP. `wp_get_scheduled_event( 'my_hook' )` returns the next scheduled instance of a hook — or `false` if it is not scheduled. `wp_next_scheduled( 'my_hook' )` returns the Unix timestamp of the next run, or `false`.

The lock — a transient named `doing_cron` — is set at the start of the cron run and expired automatically. Its purpose is to prevent two simultaneous page loads from each spawning a cron process that runs the same events twice. As we will see, this protection is weaker than it appears.

The WordPress developer documentation acknowledges these limitations directly. The [WordPress Plugin Handbook section on hooking WP-Cron into system task schedulers](https://developer.wordpress.org/plugins/cron/hooking-wp-cron-into-the-system-task-scheduler/) recommends disabling the page-load trigger with `DISABLE_WP_CRON` and replacing it with a real system cron entry — particularly for any site where consistent task execution matters.

/ The Problem

## Why WP-Cron **fails silently** in production

The traffic dependency is the most obvious issue: if nobody visits your WordPress site, scheduled events do not run. A plugin that sends a digest email at 08:00 every morning will not send if traffic to the site is zero at that moment. On low-traffic sites — internal tools, staging environments, B2B portals with off-hours downtime — this is not a theoretical edge case. It is the default outcome.

But traffic-dependent execution is not the only failure mode. There are four others that catch developers by surprise:

**Race conditions under concurrent load.** The transient-based lock assumes that only one process will check and set the lock at the same time. Under traffic spikes, two simultaneous requests can both read an unlocked state before either writes the lock. Both spawn cron processes; both run the same events. For idempotent tasks this is a nuisance. For tasks that send emails, charge cards, or write external records, it is a bug.

**DISABLE\_WP\_CRON set with no replacement.** Many managed WordPress hosts pre-set `define( 'DISABLE_WP_CRON', true );` in `wp-config.php`. Some configure a system cron replacement. Many do not, or they do and then you migrate to a new server. The result: the constant silently disables WP-Cron's traffic-triggered mechanism, and nothing takes its place. No events run. No errors appear. Scheduled jobs accumulate in the `_cron` option with timestamps that grow increasingly stale.

**PHP time limits cutting events short.** WP-Cron runs inside a PHP process, subject to `max_execution_time`. If a single cron event takes longer than the remaining time budget, PHP kills the process. The event may be partially executed, or subsequent events in the same run may never start. Because the exception is caught at the server level rather than in WordPress, no error is written to the WordPress debug log.

**Slow requests suppressing cron entirely.** `spawn_cron()` uses a transient timeout to rate-limit how often a new cron process is spawned — by default, at most once every 60 seconds. If a slow page request triggers a cron spawn and the cron process runs for 90 seconds, the next page load within that window will see the transient still set and skip spawning. On sites with slow cron events and moderate traffic, this can cause events to fire far less frequently than their registered schedule.

WP-Cron is a polite fiction: it works well enough on small, consistent-traffic sites where the cost of an occasional missed event is low. In production environments — especially those running background job queues, scheduled notifications, or webhook dispatch workers — it is not a reliable foundation.

Action Scheduler, the background processing library that ships with WooCommerce, was created specifically to address WP-Cron's limitations at scale. According to the [Action Scheduler documentation](https://actionscheduler.org/), it is now used by over 100 WordPress plugins as a reliable alternative to WP-Cron for high-volume, high-throughput scheduling — with database-backed job storage, concurrency control, and built-in failure handling.

/ Comparison

## How does WP-Cron compare to System Cron and **Action Scheduler**?

| Aspect | WP-Cron (default) | System Cron | Action Scheduler |
| --- | --- | --- | --- |
| Trigger mechanism | HTTP requests (traffic-dependent) | System daemon (time-based) | System daemon + internal queue runner |
| Reliability | Skips on zero traffic | Runs on schedule, always | Runs on schedule, recovers from PHP crashes |
| Parallel safety | Race condition under load | Single process (safe but sequential) | Locks per-action, parallel batch runners |
| Job persistence | Lost if PHP process dies mid-run | Lost if PHP process dies mid-run | Persisted in DB; survives PHP crashes |
| Failure visibility | Silent — no error log by default | System logs only | Per-action status, error messages, admin UI |
| Retry on failure | None | None (must be implemented manually) | Built-in; configurable attempts |
| Scalability | Sequential, single PHP thread | Sequential, single PHP thread | Concurrent batch processing |
| Setup complexity | Zero (built into WordPress) | Low (one crontab entry) | Requires Composer or WooCommerce |

The choice is not always obvious. A system cron job calling `wp-cron.php` is the right fix for the majority of production sites — it is simple, reliable, and requires no new dependencies. Action Scheduler becomes the right answer when you need persistent job storage, failure tracking, or parallel processing of variable-length queues.

/ Setup

## Disabling WP-Cron and configuring a **real cron job**

Configuring a reliable cron job for WordPress is a two-step process. First, tell WordPress to stop piggybacking cron on page loads. Second, configure a system cron job to call `wp-cron.php` on a fixed schedule.

**Step 1 — disable traffic-triggered execution.** Add the following constant to `wp-config.php`, above the line that reads `/* That's all, stop editing! */`:

wp-config.php — disable traffic-triggered WP-Cron

```
/**
 * Disable WP-Cron's page-load-triggered execution.
 * You MUST configure a system cron job alongside this constant,
 * or no scheduled events will run at all.
 */
define( 'DISABLE_WP_CRON', true );
```

**Step 2 — add a system cron entry.** Open the crontab for the user that runs your web server (or the www-data / nginx / apache user, depending on your server configuration):

crontab — run WP-Cron every minute via wget

```
# Run WordPress scheduled events every minute
* * * * * wget -q -O - https://example.com/wp-cron.php?doing_wp_cron >/dev/null 2>&1

# Alternative: use curl instead of wget
* * * * * curl -s https://example.com/wp-cron.php?doing_wp_cron &>/dev/null
```

Replace `https://example.com` with your actual site URL. The `doing_wp_cron` query parameter tells WordPress that this is an intended cron request, not a regular page load, so it skips the lock check and runs all due events immediately.

One minute is the standard interval. WordPress's internal scheduler can only meaningfully schedule events at intervals that are a multiple of the cron trigger frequency — so triggering every minute gives you the finest granularity available.

**WP-CLI alternative.** On servers where outbound HTTP from the cron user is blocked (or you prefer not to add HTTP overhead), WP-CLI provides a direct alternative:

crontab — run WP-Cron via WP-CLI (no HTTP)

```
# Run due events directly via WP-CLI (no HTTP request overhead)
* * * * * cd /var/www/html && wp cron event run --due-now --path=/var/www/html --url=https://example.com --quiet

# Or with an absolute path to the wp binary
* * * * * /usr/local/bin/wp cron event run --due-now --path=/var/www/html --allow-root --quiet
```

WP-CLI's approach bootstraps WordPress directly in the CLI process rather than making an HTTP call. It is faster and avoids any firewall or TLS certificate issues that might block the `wget`/`curl` approach. The trade-off is that WP-CLI must be installed and the correct PHP binary must be available in the cron environment.

In containerised environments (Docker, Kubernetes), the recommended pattern is to run a separate container or sidecar that executes `wp cron event run --due-now` on a fixed interval, targeting a shared filesystem mount. This keeps cron execution separate from your web-serving containers and allows independent scaling.

/ Custom Events

## Registering and scheduling **custom cron events**

Custom scheduled events follow a consistent pattern: register a custom schedule interval if you need something other than the built-in `hourly`, `twicedaily`, and `daily`; schedule the event on plugin activation with a guard to prevent double-scheduling; bind a callback to the event hook; and clean up on deactivation.

plugin.php — register a custom every-5-minutes cron event

```
/**
 * 1. Register a custom schedule interval.
 *    Hook: cron_schedules (filter)
 */
add_filter( 'cron_schedules', function( $schedules ) {
    $schedules['every_five_minutes'] = [
        'interval' => 300,          // seconds
        'display'  => 'Every 5 Minutes',
    ];
    return $schedules;
} );

/**
 * 2. Schedule the event on plugin activation.
 *    Guard with wp_next_scheduled() to avoid duplicate entries.
 */
register_activation_hook( __FILE__, function() {
    if ( ! wp_next_scheduled( 'my_plugin_process_queue' ) ) {
        wp_schedule_event(
            time(),                    // first run: now
            'every_five_minutes',     // schedule name
            'my_plugin_process_queue'  // hook name
        );
    }
} );

/**
 * 3. Clear the event on plugin deactivation.
 */
register_deactivation_hook( __FILE__, function() {
    $timestamp = wp_next_scheduled( 'my_plugin_process_queue' );
    if ( $timestamp ) {
        wp_unschedule_event( $timestamp, 'my_plugin_process_queue' );
    }
} );

/**
 * 4. Bind the callback to the scheduled hook.
 */
add_action( 'my_plugin_process_queue', function() {
    // Read pending jobs from your queue table and process them.
    $jobs = my_plugin_get_pending_jobs( 25 );  // batch of 25

    foreach ( $jobs as $job ) {
        my_plugin_dispatch( $job );
    }
} );
```

A few things worth being deliberate about here. The `wp_next_scheduled()` guard in the activation hook is not optional — without it, repeated plugin deactivations and reactivations will accumulate duplicate scheduled events in the `_cron` option, each firing independently. The callback will run multiple times per interval.

The deactivation hook must use `wp_unschedule_event()` with the actual timestamp, not just the hook name. `wp_clear_scheduled_hook( 'my_plugin_process_queue' )` is a simpler alternative that removes all instances of a hook regardless of timestamp — it is the safer choice if you are not certain whether one or multiple instances have been scheduled.

Keep the callback fast. WP-Cron (and even system cron) executes the callback synchronously within the PHP process. If you have a large queue, process it in batches across multiple cron runs rather than attempting to drain it entirely in one execution — PHP time limits apply regardless of how cron is triggered.

/ Action Scheduler

## When WP-Cron isn't enough: **Action Scheduler**

Action Scheduler is a background job library for WordPress, originally developed by Automattic for WooCommerce and now used by WooCommerce Subscriptions, WooCommerce Payments, and dozens of other major plugins. It is available as a standalone Composer package (`woocommerce/action-scheduler`) and ships bundled with WooCommerce.

The key difference from WP-Cron is that Action Scheduler stores jobs in a dedicated database table rather than the `_cron` option. This gives you: durable storage that survives PHP crashes, per-job status tracking (`pending`, `in-progress`, `complete`, `failed`, `canceled`), built-in retry logic, and a UI under **WooCommerce → Status → Scheduled Actions** (or directly at `/wp-admin/admin.php?page=action-scheduler`).

The API is straightforward:

action-scheduler-usage.php — schedule and handle deferred actions

```
/**
 * Schedule a one-off action to run in 30 seconds.
 * as_schedule_single_action() returns the action ID.
 */
$action_id = as_schedule_single_action(
    time() + 30,                // Unix timestamp: when to run
    'my_plugin_send_webhook',  // hook name
    [ 'order_id' => 1234 ]      // args passed to the hook
);

/**
 * Schedule a recurring action every 5 minutes.
 * Use a group name to organise actions by feature or plugin.
 */
as_schedule_recurring_action(
    time(),                      // first run: now
    300,                         // interval in seconds
    'my_plugin_process_queue',  // hook name
    [],                          // no args
    'my-plugin'                  // group (for UI filtering)
);

/**
 * Handle the scheduled hook. Action Scheduler passes the
 * scheduled args as individual parameters.
 */
add_action( 'my_plugin_send_webhook', function( $order_id ) {
    $result = wp_remote_post( 'https://endpoint.example.com/webhook', [
        'body'    => wp_json_encode( [ 'order_id' => $order_id ] ),
        'headers' => [ 'Content-Type' => 'application/json' ],
        'timeout' => 15,
    ] );

    if ( is_wp_error( $result ) || wp_remote_retrieve_response_code( $result ) >= 400 ) {
        // Throwing an exception marks the action as failed in Action Scheduler's log.
        throw new Exception(
            'Webhook delivery failed for order ' . $order_id
        );
    }
} );
```

**Queue issues to know about.** Action Scheduler's queue runner processes actions in batches. The default batch size is 25 actions per run, and the queue runner itself is triggered by WP-Cron (or your system cron). If you have a large queue — thousands of pending actions — the batch size and runner frequency become important tuning parameters. You can adjust them via filters:

action-scheduler-tuning.php — adjust batch size and concurrency

```
// Increase batch size (default: 25)
add_filter( 'action_scheduler_queue_runner_batch_size', function() {
    return 50;
} );

// Increase concurrent runners (default: 1)
// More runners = more parallel batch processing, but higher DB load
add_filter( 'action_scheduler_queue_runner_concurrent_batches', function() {
    return 3;
} );

// Extend time limit for each runner (default: 30 seconds)
add_filter( 'action_scheduler_queue_runner_time_limit', function() {
    return 60;
} );
```

**Stuck jobs.** An action gets stuck in `in-progress` status when the PHP process running it was killed before it could mark the action complete or failed. Action Scheduler has a claim timeout (default 5 minutes) after which a stuck action is released back to the queue and retried. If you see many `in-progress` actions persisting, either your PHP time limit is lower than the claim timeout, or the actions themselves are hanging on a blocking call.

**When to use Action Scheduler vs WP-Cron.** Use WP-Cron (with a real system cron behind it) for simple, low-frequency, idempotent tasks where losing a single run occasionally is acceptable. Reach for Action Scheduler when the task involves a growing queue of individual jobs, requires per-job failure tracking, or must not lose work if a PHP process dies mid-execution.

/ Debugging

## Diagnosing cron problems: **tools and techniques**

Most WP-Cron problems are invisible by default. Events that should have run 3 hours ago show no error — they simply have not run. The first step in diagnosing any cron issue is making the schedule visible.

**WP-CLI is the fastest way to diagnose a cron job for WordPress that is not running as expected.** These commands are safe to run on a live site and do not modify state:

WP-CLI — cron diagnostics reference

```
# Show all scheduled events: hook name, next run, interval, args
wp cron event list

# Show events sorted by next run time (overdue events appear first)
wp cron event list --fields=hook,next_run_relative,recurrence --orderby=next_run_timestamp

# Manually run a specific hook (useful for testing without waiting)
wp cron event run my_plugin_process_queue

# Run all overdue events immediately
wp cron event run --due-now

# Show registered schedules (intervals and their labels)
wp cron schedule list

# Delete a specific scheduled event (by hook name)
wp cron event delete my_plugin_process_queue
```

If `wp cron event list` shows events with a `next_run_relative` of "3 hours ago" or more, the cron system is not executing reliably. Check whether `DISABLE_WP_CRON` is set and whether your system cron job is actually running.

**Checking the raw cron option.** The `_cron` option holds the canonical scheduler state. You can inspect it directly from MySQL or via WP-CLI:

Check \_cron option — raw scheduler state

```
# Via WP-CLI (truncated output; use --format=json for full detail)
wp option get cron

# Via MySQL — unserialise to readable form
wp db query "SELECT option_value FROM wp_options WHERE option_name = 'cron'" | php -r "echo serialize(unserialize(file_get_contents('php://stdin')));"
```

The structure is a PHP serialised array: the outer keys are Unix timestamps (next run times), the inner keys are hook names. An event with a timestamp far in the past means it was scheduled but never executed and never rescheduled — the cron runner is not running.

**WordPress Site Health.** Navigate to **Tools → Site Health → Info → Scheduled Events**. This section reports whether WP-Cron has run within the last hour and lists any late events. It is less detailed than WP-CLI but accessible without server access.

**Action Scheduler admin UI.** If you are using Action Scheduler, the full job log is available at `/wp-admin/admin.php?page=action-scheduler`. You can filter by status (`pending`, `in-progress`, `complete`, `failed`), search by hook name or group, and manually trigger or cancel individual actions.

**Logging missed cron runs.** WP-Cron has no built-in logging. If you need to know when the cron runner actually executes — and when it does not — add a lightweight logging hook:

mu-plugins/cron-heartbeat.php — log cron execution times

```
/**
 * Log every WP-Cron execution with a timestamp.
 * Stored as a WordPress option; rotate it periodically.
 * Drop this in mu-plugins/ so it loads regardless of plugin state.
 */
add_action( 'init', function() {
    if ( defined( 'DOING_CRON' ) && DOING_CRON ) {
        $log = get_option( 'my_cron_heartbeat_log', [] );
        $log[] = current_time( 'mysql', true );
        // Keep only the last 100 entries.
        $log = array_slice( $log, -100 );
        update_option( 'my_cron_heartbeat_log', $log, false );
    }
} );
```

If you cannot see your scheduled jobs, you cannot debug them. Invest in visibility early: WP-CLI, Site Health, Action Scheduler's admin UI, and a heartbeat log will surface most cron problems within minutes. Discovering that cron has not run in 6 hours during an incident is a different experience from seeing it in a dashboard.

/ Webhook Delivery

## How cron reliability feeds **webhook delivery**

In any production webhook system, cron is the pump. A background worker — triggered on a schedule — reads jobs from a queue table, dispatches them as HTTP requests to remote endpoints, and records the result. If the pump stops, the queue fills. If the pump runs intermittently, deliveries are delayed and retries pile up out of sequence.

The failure chain looks like this: WP-Cron misfires on a low-traffic night → the queue worker does not run → 200 pending webhook jobs accumulate → when cron resumes, all 200 run simultaneously → the destination endpoint receives an unexpected spike → it starts returning 429 or 503 → the worker marks the jobs as failed → retry scheduling kicks in → the downstream system receives events hours late or not at all.

This is not a hypothetical. It is the standard failure mode for WordPress webhook systems that use the default WP-Cron scheduler. The solution is the same as for any cron reliability problem: set up a proper cron job for WordPress at the system level — one that fires every minute, regardless of traffic. With a reliable pump, the queue drains predictably and retry logic has a chance to work as designed.

There is a second, less obvious cron-related failure: the delivery window. If your webhook queue is designed to deliver events within 5 minutes of the triggering action, but WP-Cron only fires every 15–20 minutes due to traffic patterns, that SLA is broken by design — not by a bug in the queue or the endpoint. Cron interval is a first-class parameter in any webhook delivery architecture.

For a deeper look at how WordPress webhook dispatch fails — including timeout handling, silent errors, and what good delivery logging looks like — see [Why WordPress Webhooks Silently Fail in Production](https://wpwebhooks.org/blog/why-wordpress-webhooks-silently-fail-in-production/) and [Why WP-Cron Is Not Enough for Async Webhooks](https://wpwebhooks.org/blog/async-webhooks-wordpress-wp-cron-not-enough/).

/ References

## Official documentation

All implementation patterns in this article use WordPress-native APIs and documented constants. Primary references:

-   [WordPress Developer Handbook — Cron: scheduling, DISABLE\_WP\_CRON, custom schedules](https://developer.wordpress.org/plugins/cron/)
-   [wp\_schedule\_event() — function reference, parameters, return values](https://developer.wordpress.org/reference/functions/wp_schedule_event/)
-   [wp\_next\_scheduled() — check if a hook is already scheduled](https://developer.wordpress.org/reference/functions/wp_next_scheduled/)
-   [cron\_schedules filter — registering custom schedule intervals](https://developer.wordpress.org/reference/hooks/cron_schedules/)
-   [Action Scheduler — documentation, API reference, GitHub repository](https://actionscheduler.org/)
-   [WP-CLI cron command — event list, run, delete, schedule management](https://developer.wordpress.org/cli/commands/cron/)

/ Production Alternative

## If you'd rather not **maintain this yourself**

**Flow Systems Webhook Actions** is an open-source async webhook plugin for WordPress that ships with reliable queue processing, exponential backoff retry, and structured delivery logging built in. It uses Action Scheduler as its job runner, so the delivery pipeline is backed by a persistent DB queue rather than the transient-based WP-Cron lock. Delivery logs, retry triggers, and queue status are accessible programmatically via REST API — see the [REST API monitoring article](https://wpwebhooks.org/blog/wordpress-webhooks-rest-api/) and the [full endpoint reference](https://wpwebhooks.org/webhook-wordpress-plugin-api/). The plugin is free, open-source, and available on [GitHub](https://github.com/flowsystems-pl/wordpress-webhook-actions) and [WordPress.org](https://wordpress.org/plugins/flowsystems-webhook-actions/). Full details on the [async webhook plugin page](https://wpwebhooks.org/wordpress-webhook-plugin/).

## Structured data

```json
{"@context":"https://schema.org","@type":"Article","headline":"WordPress Cron Job: Why WP-Cron Fails (And How to Fix)","description":"WordPress cron job not running? WP-Cron only fires on page load. Replace it with a real system cron in two lines — full setup with WP-CLI and Action Scheduler.","datePublished":"2026-03-30","dateModified":"2026-03-30","author":{"@type":"Person","name":"Mateusz Skorupa","url":"https://wpwebhooks.org/about/"},"publisher":{"@type":"Organization","name":"WP Webhooks","url":"https://wpwebhooks.org"},"url":"https://wpwebhooks.org/blog/cron-job-for-wordpress/","image":"https://wpwebhooks.org/blog/cron-job-for-wordpress/og_image.jpg","keywords":["cron job for wordpress","wp cron","wordpress scheduled tasks","wordpress cron job setup","action scheduler","disable wp cron","wordpress background processing"]}

{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"WP Webhooks","item":"https://wpwebhooks.org/"},{"@type":"ListItem","position":2,"name":"Blog","item":"https://wpwebhooks.org/blog/"},{"@type":"ListItem","position":3,"name":"WordPress Cron Job: Why WP-Cron Fails (And How to Fix)","item":"https://wpwebhooks.org/blog/cron-job-for-wordpress/"}]}

{"@context":"https://schema.org","@type":"FAQPage","mainEntity":[{"@type":"Question","name":"Does WP-Cron run on a schedule like a real cron job?","acceptedAnswer":{"@type":"Answer","text":"No. WP-Cron is not a real scheduler. It is a pseudo-cron system that piggybacks on incoming HTTP requests. When someone visits your WordPress site, the bootstrap process checks whether any scheduled events are overdue and runs them if so. If no one visits your site, no scheduled events run — regardless of their schedule. On low-traffic sites, this means events may be delayed by minutes, hours, or indefinitely."}},{"@type":"Question","name":"How do I check if WP-Cron is running on my WordPress site?","acceptedAnswer":{"@type":"Answer","text":"Use WP-CLI: run `wp cron event list` to see all scheduled events, their next run time, and their recurrence interval. You can also inspect the raw data with `wp option get cron` to see the serialised `_cron` option. If events show next run times far in the past, WP-Cron is not executing reliably. The Site Health tool in WordPress admin (Tools → Site Health → Info → Scheduled Events) will also flag missed or failing events."}},{"@type":"Question","name":"Should I use Action Scheduler or WP-Cron for background tasks?","acceptedAnswer":{"@type":"Answer","text":"Use WP-Cron (backed by a real system cron) for lightweight, low-frequency tasks: sending a scheduled digest email, pruning old logs, running a weekly report. Use Action Scheduler when you need: persistent job storage that survives PHP crashes, parallel processing of large job batches, built-in failure tracking and retry, or a UI to inspect and manage queued jobs. Action Scheduler is the right default for any task that processes variable-length queues or must not lose jobs on failure."}},{"@type":"Question","name":"What happens to scheduled jobs when DISABLE_WP_CRON is set but no system cron is configured?","acceptedAnswer":{"@type":"Answer","text":"Nothing runs. Setting DISABLE_WP_CRON to true tells WordPress to stop piggybacking cron on page loads, but it does not create a system cron replacement. If you set this constant without also configuring a system cron entry to call wp-cron.php, all scheduled events — including core WordPress maintenance tasks, plugin jobs, and your own custom schedules — will silently stop executing. This is a common misconfiguration on managed hosting environments that pre-set the constant."}},{"@type":"Question","name":"How does an unreliable cron affect webhook delivery in WordPress?","acceptedAnswer":{"@type":"Answer","text":"In any async webhook architecture, a background worker — driven by a cron schedule — is responsible for reading queued jobs and dispatching HTTP requests to remote endpoints. If that cron job misfires or runs late, jobs accumulate in the queue unprocessed. The downstream system does not receive the event within the expected window, retries are delayed proportionally, and in severe cases the queue depth grows until memory or time limits become a problem. Reliable cron execution is not optional in a production webhook pipeline — it is the pump that keeps delivery moving."}}]}
```
