How to Replace the WordPress Cron with a Linux Cron Job

For starters, a cron is a software utility to schedule periodic script executions. Running a system backup or a security scan script are common examples of how a cron may be used. Instead of remembering to get up at 3:00am every Sunday to execute a backup script, a developer can simply automate this scheduled execution with a cron job.

Spending time in the WordPress development community, you have probably seen by now that the cool kids make use of this thing called WP Cron, or the WordPress cron system. Just like you, I wanted to be a cool kid and harness the power of this automation system. All of my favorite plugins were clearly making use of this system, after all, such as Updraft Plus regularly backing up my site and WordFence continuously scanning for vulnerabilities.

While working on high-traffic and process-heavy systems, I eventually found that the default handling of the WP Cron can be problematic, however. By default, the WordPress cron is executed on every page load—and only on page loads. This means that the WordPress cron is dependent on website traffic, can drastically slow page load times for visitors when running due jobs, and can cause harmful race conditions.

After disabling WordPress's pseudo cron system to run on page loads and instead executing with an actual cron, you should find that scheduled processes run more reliably and that your site's performance has improved. There's a lot of "gotcha" moments when configuring this change, though, so I'm going to give you everything you need to get the job done smoothly! Let's go!

Disable WP Cron on Page Loads

First, disable WP Cron from running on page loads by adding this line of code in the wp-config.php file, before the That is all, stop editing! comment line:

define( 'DISABLE_WP_CRON', TRUE );

This will disable the WP Cron from being ran on page loads. Now, without a scheduled system cron, it also means the WordPress cron jobs are not being ran. Doing this first helps us determine if our system cron is working or not because the new cron system will now be solely responsible for running the list of jobs.

Schedule a Cron Job

Hopefully, you found the first step to be relatively easy. Now it's time for the "gotcha" moments that I'm going to help you avoid! It's time to move on to the real part of this process: setting up the Linux cron job.

FYI, I'm running Ubuntu 18.04.3.

Don't Be Root

If you are running as root, don't be. You should instead add a user with root access, a sudoer. We want to ensure the system is safe and does not have permission to wreak havoc on your server through your new fancy cron job. Having a responsible user account can also help pinpoint the source of problematic code.

Additionally, you will come across this warning if you are logged in as root when using WP CLI:

Error: YIKES! It looks like you're running this as root. You probably meant to run this as the user that your WordPress installation exists under.
If you REALLY mean to run this as root, we won't stop you, but just bear in mind that any code on this site will then have full control of your server, making it quite DANGEROUS.

Check Your Event List

Now that you can safely use WP CLI, it's time to get familiar with the scheduled events on your system to know how to optimize the cron job we'll be creating soon. Checking the event list also let's us eventually confirm that the cron jobs are getting executed by our new system.

As your sudoer, cd into your WordPress root folder and run the following WP CLI command:

wp cron event list

You'll see a nice table printed out that looks like this:

+-------------------------------------+---------------------+-----------------------+---------------+
| hook | next_run_gmt | next_run_relative | recurrence |
+-------------------------------------+---------------------+-----------------------+---------------+
| jetpack_sync_cron | 2020-02-09 22:40:12 | now | 5 minutes |
| action_scheduler_run_queue | 2020-02-09 22:40:51 | now | 1 minute |
| rocket_purge_time_event | 2020-02-09 22:41:29 | now | 1 hour |
| jetpack_sync_full_cron | 2020-02-09 22:41:36 | now | 5 minutes |
| wp_privacy_delete_old_export_files | 2020-02-09 23:09:11 | 27 minutes 32 seconds | 1 hour |
| wordfence_hourly_cron | 2020-02-09 23:28:06 | 46 minutes 27 seconds | 1 hour |
| wc_admin_process_orders_milestone | 2020-02-09 23:31:10 | 49 minutes 31 seconds | 1 hour |
...

Notice that the schedule is sorted with the next scheduled runs at the top and the latest upcoming jobs at the bottom.

If you instead encountered the following error, you did not properly cd into your WordPress root folder. The root folder for your WordPress installation contains the wp-config.php file and the wp-content directory. It is typically in your HTML directory such as /var/www/html or public_html.

Error: This does not seem to be a WordPress installation.
Pass --path=`path/to/wordpress` or run `wp core download`.

Write the Job Script

Before scheduling a cron job, we need to write the script that will be scheduled for execution. Luckily, Ryan Hellyer has already done that for us. I just had to tweak it a little because of file permissions issues. Here's my full script in the home directory of my sudoer:

#!/bin/bash
clear

PATH_TO_WORDPRESS="/var/www/html"

# run cron events for each site in multisite network
for URL in = $(wp site list --fields=url --format=csv --path="$PATH_TO_WORDPRESS")
do
        if [[ $URL == "http"* ]]; then
                wp cron event run --all --due-now --url="$URL" --path="$PATH_TO_WORDPRESS"
        fi
done

# fix permissions after running cron tasks
chown -R www-data:www-data $PATH_TO_WORDPRESS

Without the last line to repair the system's file permissions, I noticed some files were returning error codes because they were owned by my cron job sudoer. The web server user www-data did not have access to read or execute files affected by cron jobs. In my case, it was some Elementor CSS files that were failing to be loaded.

If you'd like to not use WP CLI or are not running a multisite installation, you can instead just use Tom McFarlin's wget command:

wget -q -O - http://yourdomain.com/wp-cron.php?doing_wp_cron

Schedule the Cron Job

While still logged in as your sudoer, run the following command:

crontab -e

A crontab file defines a list of cron jobs owned by a user, and this command opens the current user's crontab file for editing.

Now this is where it gets tedious and where I think many people, such as myself, get caught in figuring out very sneaky issues. I'm going to first show you my crontab and then explain what's going on, so have a gander:

PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

*/5 * * * * /bin/bash /home/my-cron-user/run-wp-cron.sh

Alrighty. It looks pretty simple and benign, but let me dive into the details.

Set Your PATH Variable in Your crontab

On the first line, notice I am setting my PATH environment variable. The top-voted "gotcha" in this holy grail community posting on StackExchange, geirha points out that the cron uses its own set of environment variables when executing. This can cause commands to not be recognized when the cronjob runs because the system is using a different path than your user account.

To ensure the crontab jobs are ran with the same commands as your user, declare your PATH variable again in the crontab file. You can find your user's environment variable by echo'ing it like this:

echo $PATH

/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin

Specify the Cron Job

After ensuring the path variable is consistent, I write the cronjob:

*/5 * * * * /bin/bash /home/my-cron-user/run-wp-cron.sh

Every 5 minutes on any day, run the shell script using Bash.
*/5 means any minute per 5 steps. The asterisk (*) represents any possible value while the slash (/) is the step operator.
* means any hour.
* means any day.
* means any month.
* means any day of the week.
/bin/bash is the specified command. Note that I use the absolute path to the command in addition to already setting the $PATH environment variable because I'm paranoid.
/home/my-cron-user/run-wp-cron.sh is the argument for the command. It is the absolute path to the shell script we want to run using Bash which is the shell script that we wrote earlier to run the WordPress cron events.

Finish It Off With a Newline

As noted in the caveats section of crontab's manual page 5, "cron requires that each entry in a crontab end in a newline character." user4124 also noted this, becoming the second highest voted "gotcha" in that helpful StackExchange post, saying, "My top gotcha: If you forget to add a newline at the end of the crontab file."

Make Sure It Works

To confirm everything is working, we're going to observe the cron event list using WP CLI again. Still as your sudoer, cd into your WordPress root folder and run the WP CLI command:

wp cron event list

See if there are any jobs due soon. I personally like to keep checking the event list until there is a job due "now".

Wait until you expect your Linux cronjob to execute. Since I scheduled my cronjob to run every five minutes, I simply waited 5 minutes. If all went well, you should see that the same due "now" event has now been rescheduled.

If you wait plenty long and see many events are due "now", you'll know there is a problem. This happens because the events are waiting to be executed and thusly aren't being rescheduled.

By reviewing the links I have referenced throughout this article, you can find more information about each step. Hopefully, you'll then be able to find the answer to your situation by digging deeper. Here are the links again, for your and my convenience:

References