You've got the usual cron mess on your hands. A WordPress site missed a scheduled post, a WooCommerce store fired the same cleanup twice, or a shared host gave you no SSH access and no clear error message. In production, the problem usually isn't the line of cron syntax. It's choosing the wrong execution model, then discovering too late that WP-Cron, system cron, and queue workers solve different problems.

Table of Contents

Three Ways to Schedule PHP Code

A lot of cron confusion starts because people treat every scheduler like the same thing. It isn't. In PHP work, you usually have three paths, system crontab, WP-Cron, or an external HTTP scheduler, and the right one depends on what kind of control you have over the server.

An infographic illustrating three distinct ways to schedule PHP code: system crontab, WP-Cron, and external services.

System crontab and PHP CLI

This is the classic Linux model, cron starts the job, and PHP runs from the command line. A common pattern is an entry like * * * * * /usr/bin/php path/to/cron.php &> /dev/null, edited with crontab -e and checked with crontab -l, because cron uses five time fields and only manages the schedule, not the code itself, as described in the standard PHP cron setup pattern on Stack Overflow (cron syntax and CLI invocation). This is the best fit when you have SSH or panel access and need deterministic execution.

Practical rule: if you control the server shell, use system cron for anything that must run on time.

WP-Cron inside WordPress

WP-Cron is easier to wire into a plugin or theme, but it's traffic-driven, not time-driven. That makes it handy for low-stakes site tasks, but it's a poor fit for revenue-critical automation, especially when uptime and predictability matter. The WordPress-specific reliability problem is documented in the ACF discussion of WP-Cron, which notes the traffic tie-in and recommends alternatives when reliability matters (WP-Cron reliability limits).

External HTTP schedulers

If your host blocks shell access, an external service can hit a public endpoint on a schedule. That works well when the scheduler lives outside your server, but it also shifts responsibility to HTTP availability, authentication, and endpoint safety. For agencies, it's often the fallback when system cron isn't available and WordPress traffic alone can't be trusted.

WordPress maintenance triage patterns are useful here because scheduler choice is rarely isolated from the rest of the maintenance stack.

Building a Reliable System Crontab Entry

A good cron entry is the one you can audit later without guessing what it does or why it broke. That means absolute paths, the full PHP binary path, and output that lands in a log you can inspect when a run goes missing. Cron can fail if stdout and stderr disappear, so a tidy line that writes nowhere is a future incident waiting to happen.

Read the line as a contract

A typical entry looks like this, and the important part is not the stars, it's the command that follows them:

* * * * * /usr/bin/php /var/www/app/cron.php &> /dev/null

The schedule comes first, then the interpreter, then the script. The common PHP cron pattern on Stack Overflow follows the same structure, along with crontab -e for editing and crontab -l for checking what is installed, which is why it keeps getting copied into production setups (example cron entry and commands). If your server uses a different PHP path, find it before you paste anything into crontab.

A safer production variant sends output to a file instead of /dev/null:

* * * * * /usr/bin/php /var/www/app/cron.php >> /var/log/app/cron.log 2>&1

That single change turns a black box into something you can triage.

The PHP binary matters too. A job that runs cleanly under one version can fail under another even when the cron line stays the same, so version control belongs in the deployment checklist, not just the app code Version discipline matters too.

Validate as the right user

Most “cron doesn't work” tickets are user-context problems. The script may run fine in your shell, then fail under the account cron uses because the environment is different, the permissions are narrower, or the working directory is not what the script expects. Run the script manually from the command line before scheduling it, and run it as the same user that owns the cron entry.

If a job only works when you test it as root, it is not ready for cron.

Relative paths cause the other common failure. Cron does not care about your current directory, so include 'file.php' can break while the same script appears fine in an interactive shell. Use absolute paths for files, logs, and binaries, then check the log after the first scheduled run.

For WordPress fleets, I also treat the crontab line as part of operations, not just a scheduler entry. That means documenting which site, which PHP build, and which runner owns the job, because the same command can behave differently across servers, container images, and shared hosting panels.

Why WP-Cron Is Not Real Cron

WP-Cron looks convenient until the site gets busy, goes quiet, sits behind aggressive caching, or hits all three at once. It does not fire because a clock says it should. It runs only when WordPress receives a request and sees that an event is due, so traffic directly affects whether scheduled work happens. The ACF guidance calls out the trade-off clearly, WP-Cron is tied to site traffic and can hurt reliability on setups that depend on predictable execution, which is why production teams usually move scheduled work elsewhere (WP-Cron and traffic dependence).

A comparison chart explaining why WP-Cron is different from traditional system cron jobs regarding performance and reliability.

Where WP-Cron still fits

WP-Cron is acceptable for light housekeeping on a small site. If a plugin schedules a background check, a cache clear, or a low-risk email queue on a blog with steady visits, the convenience can outweigh the limitations. It also helps when you do not control the server and need a WordPress-native scheduler without adding more infrastructure.

Where it falls apart

Production stores are a different problem. A WooCommerce site that depends on timed order processing, stock cleanup, or follow-up emails cannot rely on a scheduler that wakes up only when traffic cooperates. Low-traffic sites have the opposite failure mode, scheduled jobs sit waiting for the next request, so work that should run on time starts to drift even though the code itself is fine.

The video below is useful if you want a visual rundown of the failure modes and the trade-off between convenience and control.

For production work, the decision is straightforward. If the job matters when visitors are not present, do not rely on WP-Cron alone.

Disabling WP-Cron and Replacing It With System Cron

The clean replacement is to turn off WordPress's internal trigger and let the server handle scheduling. In wp-config.php, add the standard constant:

define('DISABLE_WP_CRON', true);

That stops WordPress from trying to kick off scheduled events on page load. Then add a real cron entry that calls wp-cron.php on a cadence your site can handle, often every five minutes for housekeeping and tighter only where the workload needs it.

A typical server-side entry looks like this:

*/5 * * * * /usr/bin/php /var/www/app/wp-cron.php >> /var/log/app/wp-cron.log 2>&1

Use a cadence that matches the workload

Don't force every scheduled task onto the same interval. Order processing may deserve a shorter cadence than general maintenance, especially if you're watching a commerce site. The important part is that the schedule is explicit and observable, instead of depending on anonymous traffic.

Know the fallback mode

Some hosts don't let you edit crontab. In those cases, WordPress's alternate cron behavior can be a bridge, but it's still a compromise, not a true replacement for server-level scheduling. It can help on restricted hosting, yet the operational trade-off remains the same, you're trying to make a request-driven system behave like a clock.

If you test manually, test the same path the scheduler will use. That means the specific PHP binary, the exact file path, and the same user context. Problems with object cache, permissions, or missing file writes often show up only when the cron hook finally tries to do the work, not when the page loads.

For hosts that hide the shell but expose WordPress control, the best move is to treat WP-Cron as a compatibility layer, not an answer.

Locking and Queues for Jobs That Run Too Long

A cron interval is a blunt tool, PHP workloads are not. A digest job, export routine, or sync process can run longer than the interval that starts it, and once that happens the next invocation can collide with the first. In production, that is how duplicate rows, repeated emails, and half-finished exports show up.

A diagram illustrating how locking and queuing mechanisms prevent concurrent execution for long-running cron jobs in PHP.

Start with mutual exclusion

A lock file is the simplest guardrail. Create a file in /tmp, check for it at the start of the job, and exit if another run already holds the lock. It is crude, but it often stops two overlapping invocations from touching the same records.

When the job already lives in MySQL-heavy code, GET_LOCK() is often the cleaner fit. The source material shows a pattern with a 5-second timeout, which makes the intent clear, only one job instance should select a customer or process a row at a time (locking and queue handoff guidance).

Practical rule: if the job can overlap, add a lock before you scale the schedule.

Hand off work that does not belong in cron

When the script is pushing real volume, cron should decide what starts, not do all the work itself. The large-workload guidance in the source recommends splitting monolithic jobs into smaller scripts and handing heavy processing to queue workers so multiple workers can run in parallel. That keeps the scheduler simple and moves the slow part into a system that can absorb retries and concurrency pressure.

A better way to frame it is simple. If a job cannot finish comfortably inside the interval that launches it, it is not really a cron job anymore, it is a queue task with a cron trigger. That difference matters because the failure mode changes from missed schedule to backlog management.

Use stale ownership when locks are not enough

Some systems mark a record as owned, then treat it as stale after a short time window. The source mentions implementations that consider records stale after about 2 minutes or 30 seconds, depending on the design. That helps when workers die mid-job and a hard lock would otherwise strand work forever.

The strongest production habit is boring. Keep cron thin, keep jobs single-purpose, and move anything long-running into a queue where retries, concurrency, and visibility are easier to manage.

Logging, Monitoring and Alerting Missed Runs

A cron entry that fires and leaves no trace is a liability. It creates the illusion of reliability, then hides the exact detail you need when something slips. Cron should be treated as an execution trigger, not a guarantee of completion, and every job needs monitoring outside the job itself because background failures often have no visible user impact.

A four-step infographic illustrating a process for logging, monitoring, and alerting for missed automated job runs.

Write every run somewhere useful

For CLI scripts, send output to a log file and include timestamps in the application's own messages. For HTTP-triggered jobs, a clean success signal matters just as much. If you are running scheduled work through a host or platform layer, the handler still needs to return a clear success status, and failures should be retryable when the scheduler supports it. That is the same basic contract you see in systems that treat a non-2xx response as a failed run and allow retries through their own job configuration.

Watch for silence, not just errors

Missed runs are often more dangerous than explicit failures. A cron that stops logging entirely may mean the scheduler died, the command path changed, or the host stopped invoking the job. Heartbeat checks solve a different problem than log review. They let you alert on absence instead of waiting for a user to complain.

For WordPress fleets, that absence is usually the first sign that something is off across several sites at once, not just one broken task.

Keep the failure path visible

Test regularly, capture errors, and do not assume the scheduler will tell you what happened. If a job exits non-zero, make sure something outside the job notices. In a multi-site setup, that visibility is usually more useful than another verbose log file, because you need to know which site missed a run first, not just that some cron task failed somewhere. I have seen teams lose hours because the logs existed, but nothing aggregated them into an alert the on-call engineer could act on.

A simple operational pattern works here. Log the run, monitor for the next expected heartbeat, and alert when the heartbeat goes missing. That keeps failure triage fast and turns a silent cron issue into something you can route, page, and fix before the backlog spreads.

The habit that separates stable setups from fragile ones is straightforward. Every cron job should leave evidence, every failure should be observable, and every missed run should become an alert, not a mystery.

If you are deciding how much of this belongs in the host layer versus the app layer, WordPress hosting considerations is the kind of background that helps before you commit to a scheduler model.

Hosting Realities and Containerized Schedulers

The shell-based cron model still works, but it's no longer universal. Shared hosts, managed WordPress platforms, and containerized deployments all change the control plane, which means the old advice only applies when the underlying environment still exposes a writable crontab. The hosting guide from the publisher's broader WordPress material is worth keeping in mind if you're deciding what kind of environment you run (WordPress hosting considerations).

Shared hosting and cPanel

On shared hosting, the control panel often exposes a cron UI instead of raw SSH. That's fine as long as you still check the basics, the command runs under the right account, the PHP binary path is the one the host expects, and the script itself is written for CLI use. If the panel offers both php and php-cli, don't guess, use the one intended for command-line execution.

Managed WordPress and platform schedulers

Managed hosts often wrap cron in their own tools, because they don't want customers editing system cron directly. That doesn't change the logic of the job, it only changes where the schedule lives. In that setup, the right move is to use the host's scheduler for timing and keep the PHP task itself small, idempotent, and easy to log.

Containers and deployment-defined schedules

In containerized environments, cron often isn't a daemon on the box at all. A strong example comes from Google App Engine's legacy PHP model, where cron is defined separately as deployment configuration in cron.yaml, with an HTTP handler, a schedule, and retry parameters rather than a local shell command (deployment-defined cron configuration). That pattern maps well to modern platform schedulers and Kubernetes CronJobs, because the schedule is declared alongside deployment rather than hidden on a machine you don't really control.

For a team running multiple sites, the operational question is simple. Can you guarantee execution where the app lives, or do you need to push scheduling out to the platform? If the answer is the latter, treat cron as infrastructure, not a plugin feature, and design the job around observability, locks, and retries.


If your WordPress fleet has missed runs, duplicate jobs, or cron tasks that only work on some hosts, WP Triage can help you sort out what's breaking first and what needs attention next. Visit WP Triage to compare risk across your sites, prioritize the fixes that matter, and bring some order to the maintenance queue.