Replace WP-Cron with a real cron job (cPanel, Plesk, WP-CLI)
Fix late WordPress tasks with a tested server cron: cPanel, Plesk and WP-CLI setup, private backups, overlap control, verification and rollback.

On this page
- What changes when you replace WP-Cron?
- Choose the runner before editing anything
- Step 1: Make a private backup of wp-config.php
- Step 2: Capture the current state
- Step 3A: Add the task in cPanel
- Step 3B: Use Plesk
- Step 4: Disable the visitor trigger
- Step 5: Verify three different things
- Long jobs: prevent overlap deliberately
- Troubleshooting without guessing
- Roll back in the right order
- What to send your hosting team
- Reader questions
- Sources & further reading
The short answer
Check whether your host already runs cron. Create and verify one replacement runner through cPanel, Plesk or WP-CLI, then set DISABLE_WP_CRON to true before WordPress loads. Keep configuration backups outside public folders. Verify automatic runs, due callbacks and their actual results; an HTTP 200 alone is not enough. Use the right PHP environment, monitor failures and prevent overlapping local runners where appropriate.
Before you start
Access to Cron Jobs or Scheduled Tasks; safe file access to wp-config.php; a configuration backup outside every public document root; the real site path and final HTTPS URL; optional SSH/WP-CLI. Confirm whether the host already provides a runner and which minimum interval your plan permits.
A scheduled post sits there at 9:00, still unpublished. You open the website to investigate, and suddenly it appears. That is a familiar sign that WordPress is relying on visits to start its scheduled work.
A server cron job gives that work an independent trigger. The safe sequence is simple: check for an existing runner, create and test the replacement, then disable the visitor trigger. Afterward, verify an actual task, not just an HTTP status code.
This guide covers cPanel, Plesk and WP-CLI. It also explains the parts that a copy-and-paste command cannot solve: PHP compatibility, overlapping jobs, blocked requests and a scheduler that appears healthy while a plugin keeps failing.
What changes when you replace WP-Cron?
WordPress keeps a list of scheduled events. WP-Cron checks that list and starts due callbacks; its default trigger depends on a request reaching WordPress. A quiet site may therefore run work late. A fully cached page served without reaching WordPress may not provide that trigger either.
A system scheduler calls the runner at a chosen interval, regardless of visitors. You keep WordPress's events and plugin callbacks. You replace the mechanism that starts them.
The WordPress cron handbook explains this request-driven behavior. It does not promise that every callback will succeed, retry safely or finish before the next tick. Those are separate questions.
For example, a job due at 09:01 with a runner scheduled at 09:00, 09:05 and 09:10 normally gets its next opportunity at 09:05. Server load, a previous run or a failing callback can add delay. A five-minute interval is an opportunity every five minutes, not a five-minute delivery guarantee.

The event list stays in WordPress. The server supplies the trigger; the application still has to do the work.
Choose the runner before editing anything
Ask your host whether a scheduler is already configured. Managed hosting, WP Toolkit and a previous developer may have installed one. Adding a second task can create duplicate attempts or unnecessary load.
Your setup | Start here | What to check |
|---|---|---|
Plesk with WP Toolkit | Its takeover feature | Replacement task exists and has the right interval |
cPanel without WP-CLI | HTTP request to wp-cron.php | CDN, firewall and authentication allow the request |
SSH with WP-CLI | Run due events locally | Correct site user, PHP, paths and overlap control |
Host-managed runner | Ask the host to confirm it | Frequency, monitoring and who handles failures |
HTTP uses the website's PHP environment but travels through web security rules. WP-CLI avoids that HTTP path, but its PHP version and environment can differ from the website. Neither route fixes a broken plugin callback by itself.
Pick an interval according to the work. Five minutes is a reasonable starting example when a few minutes of delay is acceptable; fifteen minutes may suit less time-sensitive work. For minute-sensitive jobs, ask whether your plan supports a one-minute runner and measure its cost. Payment, inventory and email workflows may use additional queues: check the relevant plugin's requirements before assuming WP-Cron handles everything.
Step 1: Make a private backup of wp-config.php
The configuration file contains database credentials and security keys. Keep its backup outside every public web directory, or download it to a protected location on your computer. A file called wp-config.php.bak in public_html can be served as plain text on some servers.
In File Manager, copy the file to a private directory above your website's document root. Confirm that the directory is not an alias or document root for another domain. If you cannot establish that, use a local download instead.
For SSH, this example assumes /home/example/private-backups is outside all document roots. Replace the account and site paths first.
mkdir -p /home/example/private-backups
chmod 700 /home/example/private-backups
install -m 600 /home/example/public_html/wp-config.php \
/home/example/private-backups/wp-config-before-cron.phpThis is a configuration rollback copy. It is not a substitute for a tested site and database backup. WordPress's hardening guide covers protecting configuration, permissions and backups.
Step 2: Capture the current state
With WP-CLI, use the real installation path:
wp --path=/home/example/public_html cron event list \
--fields=hook,next_run_gmt,next_run_relative,recurrence
wp --path=/home/example/public_html cron testThe event list command lets you inspect due times. Compare next_run_gmt with the current UTC time rather than relying only on a friendly relative label. Record any overdue hooks and the plugin that owns them.
The test command checks WordPress's normal HTTP spawning mechanism. Before migration it can help identify loopback failures. Once DISABLE_WP_CRON is true, its disabled-cron error is expected; it does not test your external runner.
No SSH? Use a compatible cron inspection plugin or ask your host for the current event list. Also record one real task you can verify safely later.
Step 3A: Add the task in cPanel
Open Cron Jobs using cPanel's search. The host can hide this interface, so ask support if it is missing.
Under Add New Cron Job, choose the interval. For every five minutes, use */5 for Minute and * for Hour, Day, Month and Weekday. The command field contains only the command below; do not paste the five timing fields into it.
Keep Cron Email enabled during setup, or send output to a private log. cPanel's Cron Jobs documentation explains the timing fields, executable paths and notifications.
Option A: Call the public cron endpoint
Use the final HTTPS hostname and the correct WordPress directory. A subdirectory installation may need /wordpress/wp-cron.php.
/usr/bin/curl --fail --silent --show-error \
--connect-timeout 10 --max-time 60 \
--output /dev/null https://example.com/wp-cron.phpReplace /usr/bin/curl with the path your host provides. This request reports HTTP errors and connection problems while discarding the response body. It intentionally does not follow redirects: use the canonical URL and fix an unexpected redirect during testing.
The curl manual documents these options. Its 60-second limit caps the client request, not necessarily the lifetime of PHP work on the server. A timeout can mean “the outcome is unknown”; do not immediately run a long job again without checking its state.
Do not put dashboard passwords into the cron command. If a staging site's access protection blocks the URL, use an approved local runner or a host-supported exception scoped to this endpoint. Avoid turning off the whole firewall.
Option B: Run due events with WP-CLI
Find the installed paths and inspect PHP:
command -v wp
wp cli infoThen test the command under the site's normal system user:
/usr/local/bin/wp --path=/home/example/public_html \
cron event run --due-nowThe path is an example, not a promise about your host. The WP-CLI command runs events that are due. Avoid --all for a recurring runner: it can execute future events early. Avoid --allow-root as a routine workaround; use the site's account and correct permissions.
Cron often has a smaller PATH than an interactive shell. Ask the host to configure the PHP binary used by WP-CLI, then test that same environment. A web PHP selector does not automatically change command-line PHP. Our PHP version check and migration guide covers that distinction in more detail.
Step 3B: Use Plesk
WP Toolkit takeover
Open WordPress, find the installation card and enable Take over wp-cron.php. Current WP Toolkit documentation says a replacement scheduled task is normally created every 30 minutes.
Check that Create a replacement task when takeover is initiated is enabled, or confirm that another runner already exists. The switch can disable the default trigger without providing a replacement if that option is off.
Use the icon beside the switch to inspect the generated task and change its schedule if needed. Check for a pre-existing manual task before keeping both.
A manual scheduled task
Go to Websites & Domains > Scheduled Tasks > Add Task. Choose Fetch a URL for the HTTPS cron endpoint, Run a PHP script for the local file, or Run a command for a supported command-line runner. Use Run Now before saving and leave failure notifications on.
Plesk's task documentation notes that command tasks on Linux are chrooted by default. A path that works over administrator SSH may not exist inside that environment. Use the subscription's permitted paths or ask the host; do not loosen the jail just to make an example command work.
Plesk's task time zone and WordPress's Settings > General time zone are separate. Verify both when troubleshooting a post that seems an hour early or late.
Step 4: Disable the visitor trigger
After the replacement command works and the scheduled task has actually run, edit wp-config.php. Search for DISABLE_WP_CRON first; update an existing definition rather than adding a duplicate.
define( 'DISABLE_WP_CRON', true );Place it in the custom settings area, above the familiar “stop editing” comment and, crucially, before WordPress loads wp-settings.php. The comment is a guide for humans; the PHP loading order is what matters. See the configuration handbook.
Load the site and admin area after saving. Restore the private copy if you introduced a syntax error. If WP Toolkit handled takeover, check its generated configuration rather than adding another definition.
The official system scheduler guide uses this constant to stop automatic page-load spawning. It does not block direct access to wp-cron.php. An HTTP runner still needs that endpoint; protecting it is a separate web-server or firewall decision.
Step 5: Verify three different things

A healthy request is only the first check. Verify the event and its result too.
- The scheduler started the command. Check its execution history, cron email or a timestamped private log across at least two automatic ticks. “Run Now” proves a manual run, not that the schedule fires.
- WordPress processed due work. Compare the event list before and after a tick. Recurring events should advance; investigate old due times that persist. Read PHP errors and plugin logs.
- The intended result exists. On staging, schedule a harmless test post and confirm its published status without first visiting the front end. For a backup or email plugin, check the produced backup or delivery log instead.
Do not create an arbitrary cron_test hook and assume that its disappearance proves useful execution. An event needs a registered callback. Likewise, a HEAD request or HTTP 200 can indicate a reachable endpoint without proving a particular callback finished.
A scheduled production test post is public content. Use staging, an already-approved post or an application log when publishing a dummy item would be inappropriate.
Long jobs: prevent overlap deliberately
WordPress's wp-cron.php source uses a time-based cron lock. It is not an unlimited lock held until every job finishes. WP-CLI's due-event command should not be assumed to inherit that HTTP runner's locking behavior.
For a local Linux WP-CLI runner, a host-supported flock wrapper can prevent concurrent invocations that use the same lock file. First create a private writable directory outside the web roots. This example assumes the paths and PHP environment have already been checked:
/usr/bin/flock --nonblock /home/example/private-cron/site.lock \
/usr/local/bin/wp --path=/home/example/public_html \
cron event run --due-nowUse this as the command instead of a second unlocked WP-CLI task. A competing run skips when the lock is held. Monitor repeated skips: preventing overlap does not make a stalled job healthy.
The util-linux manual notes filesystem limitations, including some NFS/CIFS setups. A lock on one server does not automatically coordinate several servers. Ask a developer about shared locks and idempotent callbacks for a multi-node site.
Do not use a larger WP_CRON_LOCK_TIMEOUT as a universal fix. Measure the slow callback, check the plugin's own queue and locking, and fix the bottleneck.
Troubleshooting without guessing
What you see | Check first | Useful next action |
|---|---|---|
HTTP 403, 401 or challenge page | WAF, access protection and endpoint URL | Scope an approved exception or use WP-CLI |
HTTP 301 or 302 | Wrong scheme, hostname or directory | Use the final URL; confirm a GET reaches PHP |
wp or php not found | Cron environment and executable paths | Use verified paths and compatible CLI PHP |
HTTP 200 but work stays overdue | PHP errors, locks and callback failures | Check event times and application logs |
Repeated overlapping runs | Duplicate tasks and job duration | Keep one runner and add suitable locking |
One multisite subsite stays late | Selected site URL | Run and verify each subsite separately |
For multisite, use --url=https://subsite.example.com to select the intended site. One call for the main site is not a complete network plan. Keep a maintained task per site or ask a developer for a network runner that discovers sites, handles failures and controls concurrency.
Keep errors visible after setup. If you redirect output, use a private log with rotation or a monitoring service that alerts on missed successful runs. Silencing all output can hide the day a PHP change breaks the runner.
If a callback sends email, takes payment or changes inventory, retries need application-level safeguards. A scheduler cannot decide whether repeating a partially completed action is safe.
Roll back in the right order
Restore normal visitor-triggered spawning by removing the true DISABLE_WP_CRON definition or setting it to false. Then remove the external task. For WP Toolkit, use its takeover controls and inspect the resulting scheduled task and configuration.
Finally, verify a real scheduled action again. Restoring the old trigger does not cure the low-traffic limitation; it restores the previous behavior while you diagnose the replacement.
What to send your hosting team
Ask them to confirm the site path, system user, compatible CLI PHP or final HTTP URL, interval, overlap policy and failure alerts. Request evidence from two automatic runs and one real callback result before disabling the visitor trigger.
That small handover is more useful than “cron added.” It tells the next person how the work starts, how to prove it finished and where to look when it stops.
Reader questions
Does DISABLE_WP_CRON block direct requests to wp-cron.php?
No. It disables WordPress's automatic visitor-triggered spawning. A server request can still call wp-cron.php. Restricting that endpoint is a separate web-server or firewall decision and must not block an HTTP runner you rely on.
How often should the replacement task run?
Choose according to acceptable delay, job duration and the host's limits. Five or fifteen minutes are useful starting examples, but minute-sensitive work may need a faster supported interval. No interval guarantees that a failing or stalled callback completes on time.
Why does wp cron test fail after migration?
That command checks the built-in spawning path. It reports DISABLE_WP_CRON when the visitor trigger is disabled, which is expected. Verify the external scheduler through its automatic execution history and an actual callback result.
Is an HTTP 200 response enough to prove cron works?
No. It establishes an HTTP response, which can be empty, challenged or unrelated to successful callbacks. Check due-event times, PHP/plugin logs and a real result such as an approved scheduled post or generated backup.
Can a long WP-CLI cron run overlap the next one?
Yes, unless suitable coordination prevents it. A local flock wrapper can protect runners sharing the same lock file on supported filesystems. Multi-server setups need a coordinated locking plan, and recurring skipped runs still need investigation.
How do I reverse the change?
Restore normal spawning by removing the true DISABLE_WP_CRON definition or changing it to false, then remove the external task. For WP Toolkit, use its takeover controls and inspect both configuration and tasks. Verify a scheduled action afterward.
Sources & further reading
What changed
Reworked the migration around test-before-disable order. Replaced public-folder config backups with private copies; corrected timing, HTTP 200 and cron-lock claims; added PHP environment checks, WP Toolkit replacement-task checks, scoped troubleshooting, overlap control, multisite coverage and end-to-end verification. Added a new cover and two original diagrams.
Originally published . About our editorial updates.


