WordPress has a built-in task scheduler called wp-cron. It handles scheduled events like publishing future-dated posts, checking for plugin updates, running scheduled backups, and processing WooCommerce orders. The problem is how it works: wp-cron runs by piggybacking on visitor page loads. Every time someone visits your site, WordPress checks if any scheduled tasks are due and fires them.
This design has real consequences. On low-traffic sites, tasks run late because nobody visits to trigger them. On high-traffic sites, wp-cron adds an extra PHP execution on nearly every page load, consuming PHP-FPM workers that should be serving actual visitors. The fix is straightforward: disable wp-cron’s web-triggered behavior and replace it with a real system cron job that runs WP-CLI on a schedule.
The problem with wp-cron.php#
When a visitor loads any page on your WordPress site, WordPress makes an asynchronous HTTP request to
wp-cron.php
to check for and run pending scheduled tasks. This happens on every page load, regardless of whether any tasks are due.
Why this is wasteful
Each call to wp-cron.php is a full PHP execution. It loads the entire WordPress stack, checks the database for pending events, and runs any that are due. On a site with 1,000 daily visitors, that is up to 1,000 extra PHP executions per day just to check a scheduler. The actual scheduled tasks might only need to run a handful of times.
On shared hosting or servers with limited PHP-FPM workers, these extra executions compete with real page loads. During traffic spikes, the additional wp-cron.php requests can push the server past its PHP worker limit, resulting in 503 errors for actual visitors.
Why tasks run late on low-traffic sites
If nobody visits your site for six hours, no scheduled tasks run for six hours. A post scheduled to publish at 8:00 AM does not actually publish until the next visitor arrives, which might be 10:00 AM. A backup plugin scheduled for 3:00 AM never runs at 3:00 AM because nobody is browsing your site at that hour.
This is a fundamental limitation of the web-triggered design. wp-cron has no independent process keeping time. It relies entirely on traffic to trigger it.
What WP-CLI is#
WP-CLI is the command-line interface for WordPress. It lets you manage WordPress from the terminal without using a browser. You can update plugins, manage users, import content, run database operations, and interact with WordPress’s internal APIs directly from the command line.
For the cron replacement, the relevant command is:
wp cron event run --due-now
This does exactly what wp-cron.php does on a page load: it checks for scheduled events that are due and runs them. The difference is that it runs as a CLI process, not as a web request. It does not consume a PHP-FPM worker, does not compete with visitor traffic, and runs on a schedule you control.
Installing WP-CLI
If WP-CLI is not already installed on your server:
curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
chmod +x wp-cli.phar
sudo mv wp-cli.phar /usr/local/bin/wp
Verify:
wp --info
This prints the WP-CLI version, PHP version, and other details. If it works, WP-CLI is ready.
Step 1: Disable wp-cron.php#
Open
wp-config.php
in your WordPress root directory and add:
define('DISABLE_WP_CRON', true);
This must go before the line that says
/* That's all, stop editing! */
.
What this does: it tells WordPress to stop making the asynchronous HTTP request to wp-cron.php on every page load. Scheduled events are still registered in the database. They just do not get triggered automatically by web traffic anymore. You are taking over responsibility for triggering them.
What this does not do: it does not remove wp-cron.php from the server. The file still exists and still responds to direct HTTP requests. It just stops being called automatically.
Step 2: Set up the cron job#
The crontab entry
Open the crontab:
crontab -e
Add this line:
*/5 * * * * cd /var/www/html && /usr/local/bin/wp cron event run --due-now --quiet >> /var/log/wp-cron.log 2>&1
Save and exit. The cron daemon picks up the change immediately.
Breaking down the command
*/5 * * * *
– run every 5 minutes. This is the standard interval. WordPress’s built-in scheduler checks on every page load, so every 5 minutes is more than adequate for most sites. See Cron job syntax: a practical guide with examples for the full explanation of the five-field syntax.
cd /var/www/html
– change to the WordPress directory. WP-CLI needs to be run from the WordPress root, or you need to specify
--path=
. Using
cd
is simpler.
&&
– only run the next command if
cd
succeeds. If the directory does not exist (renamed, unmounted), the WP-CLI command is not executed.
/usr/local/bin/wp
– the full path to WP-CLI. Cron jobs run with a minimal PATH that may not include
/usr/local/bin
. Always use the full path.
cron event run --due-now
– run all scheduled events that are currently due. Events that are not yet due are left alone.
--quiet
– suppress non-error output. Without this, every successful run generates output that cron tries to email.
>> /var/log/wp-cron.log 2>&1
– append both standard output and error output to a log file. This captures any errors so you can debug if something goes wrong.
Which user should the cron job run as?
The cron job should run as the same user that owns the WordPress files, which is typically
www-data
on Ubuntu/Debian:
sudo crontab -u www-data -e
Running as the wrong user causes permission issues. If WP-CLI runs as root but WordPress files are owned by
www-data
, any files WP-CLI creates (cache files, upload directories) will be owned by root. WordPress (running as
www-data
through PHP-FPM) cannot write to them, leading to silent failures.
If you are unsure which user owns your WordPress files:
ls -la /var/www/html/wp-config.php
The third column is the owner. Use that user for the cron job.
Alternative: using –path instead of cd
*/5 * * * * /usr/local/bin/wp --path=/var/www/html cron event run --due-now --quiet >> /var/log/wp-cron.log 2>&1
The
--path
flag tells WP-CLI where WordPress is installed. This is equivalent to the
cd
approach but keeps the command on one line without chaining.
Step 3: Verify it works#
Check that scheduled events are running
After setting up the cron job, wait at least 5 minutes, then check:
wp cron event list --path=/var/www/html
This shows all registered scheduled events, their schedule, the next run time, and whether they have run recently. Events that should have run in the last 5 minutes should show an updated “next run” time.
Check the log file
tail -20 /var/log/wp-cron.log
If the cron job is working, the log file contains output from each run. With
--quiet
, successful runs produce no output, so an empty or absent log file after several runs means everything is working. Errors are still logged.
Check the system cron log
To confirm that the cron daemon is executing the job:
grep wp /var/log/syslog
Or on CentOS/RHEL:
grep wp /var/log/cron
You should see entries showing the cron job executing every 5 minutes.
Test manually
Run the command manually to confirm it works before relying on cron:
cd /var/www/html && sudo -u www-data /usr/local/bin/wp cron event run --due-now
If this produces errors, fix them before adding the cron job. Common issues:
- WP-CLI cannot find WordPress (wrong path)
- Database connection fails (wp-config.php not readable by the user)
- Permission errors (running as wrong user)
Running specific WP-CLI commands on a schedule#
The cron replacement above handles WordPress’s internal scheduled events. But you can also schedule any WP-CLI command to run on a cron schedule.
Database optimization
0 3 * * 0 cd /var/www/html && /usr/local/bin/wp db optimize --quiet >> /var/log/wp-db-optimize.log 2>&1
Runs
OPTIMIZE TABLE
on all WordPress tables every Sunday at 3:00 AM. This reclaims unused space and defragments tables after heavy write activity.
Delete spam comments
0 4 * * * cd /var/www/html && /usr/local/bin/wp comment delete $(/usr/local/bin/wp comment list --status=spam --format=ids --path=/var/www/html) --force --quiet 2>> /var/log/wp-spam-cleanup.log
Deletes all spam comments daily at 4:00 AM. Without this, spam comments accumulate in the database and slow down queries.
Delete post revisions
0 5 1 * * cd /var/www/html && /usr/local/bin/wp post delete $(/usr/local/bin/wp post list --post_type=revision --format=ids --path=/var/www/html) --force --quiet 2>> /var/log/wp-revision-cleanup.log
Deletes all post revisions on the 1st of every month. On sites with heavy editing, revisions can make up the majority of database rows.
Update plugins
0 6 * * 1 cd /var/www/html && /usr/local/bin/wp plugin update --all --quiet >> /var/log/wp-plugin-updates.log 2>&1
Updates all plugins every Monday at 6:00 AM. Use this with caution. Automatic updates can break a site if a plugin update introduces a bug or incompatibility. Only set this up if you have a monitoring system that catches failures quickly, or if you are running it on a staging environment first.
Flush object cache
*/30 * * * * cd /var/www/html && /usr/local/bin/wp cache flush --quiet 2>> /var/log/wp-cache-flush.log
Flushes the object cache every 30 minutes. This is a blunt instrument and usually not necessary. It is sometimes useful when debugging cache-related issues or when a plugin does not invalidate its cache properly.
Run all examples as the correct user
All the above examples should run as the WordPress file owner. If using
sudo crontab -u www-data -e
, the jobs run as
www-data
automatically. If using the root crontab with the system crontab format (
/etc/crontab
), add the username:
0 3 * * 0 www-data cd /var/www/html && /usr/local/bin/wp db optimize --quiet
Multisite considerations#
On a WordPress multisite installation, WP-CLI commands default to the main site. To run cron events for all sites in the network:
*/5 * * * * cd /var/www/html && /usr/local/bin/wp site list --field=url | xargs -I {} /usr/local/bin/wp cron event run --due-now --url={} --quiet 2>> /var/log/wp-cron.log
This lists all site URLs in the multisite network and runs due cron events for each one. Without specifying
--url
, only the main site’s events are processed.
Troubleshooting#
“Error: This does not appear to be a WordPress installation.”
WP-CLI cannot find WordPress at the current directory or the specified
--path
. Check:
ls /var/www/html/wp-config.php
If the file does not exist at that path, update the path in the cron job.
“Error establishing a database connection”
The database is not accessible from the CLI context. This can happen if wp-config.php uses
localhost
for the database host and the MySQL socket is not in the expected location. Check:
cd /var/www/html && wp db check
“Permission denied” errors
The cron job is running as a user that does not have read access to the WordPress files. Check file ownership and ensure the cron job runs as the correct user.
Tasks still not running on time
After setting up the cron job, if tasks are still delayed:
- Confirm
DISABLE_WP_CRONis set totruein wp-config.php (notfalseor missing) - Confirm the cron job is in the crontab:
crontab -l - Check the system log for execution:
grep CRON /var/log/syslog - Run the command manually to see if it produces errors
WP-CLI uses a different PHP version
WP-CLI uses whatever
php
binary is in the PATH. If your server has multiple PHP versions and WP-CLI picks the wrong one, specify the PHP binary explicitly:
*/5 * * * * cd /var/www/html && /usr/bin/php8.3 /usr/local/bin/wp cron event run --due-now --quiet >> /var/log/wp-cron.log 2>&1
WordPress cron on Hostney#
On Hostney, cron jobs are managed through the control panel under the Cron Jobs section. Currently, Hostney’s cron scheduler supports web-based calls (HTTP requests to a URL), not direct CLI commands. This means you cannot run WP-CLI directly from the cron scheduler.
To replace wp-cron.php with a scheduled cron job on Hostney:
- Add
define('DISABLE_WP_CRON', true);to wp-config.php - Go to Cron Jobs in the control panel
- Set the schedule to every 5 minutes
- Set the URL to:
https://yourdomain.com/wp-cron.php?doing_wp_cron
This triggers wp-cron.php on a fixed schedule via HTTP request rather than relying on visitor traffic. The result is the same: scheduled tasks run reliably every 5 minutes regardless of site traffic. The difference from the WP-CLI approach described above is that the request goes through the web server and consumes a PHP-FPM worker briefly, but since it runs on a fixed interval rather than on every page load, the overhead is minimal.
For SSH access to your container (useful for running WP-CLI commands manually), see the Terminal Access section in the control panel.