In this tutorial, I will show you how to optimize WordPress performance by replacing the default WP-Cron triggering mechanism with Crontab on Linux.

WordPress uses WP-Cron to run scheduled tasks and it has worked for many users.

But the way it works is sub-optimal.

On every page load, WP-Cron checks the list of scheduled tasks and performs any tasks due to run.

Triggering scheduled tasks by page load has two weaknesses:

1. It might affect page loading speed

2. Indeterministic schedule – e.g. Task A is scheduled to run every hour but there’s no page load in six hours. Task A is only executed six hours later.

In other words, we need a better way to trigger WP-Cron that doesn’t rely on page load. And the best solution is perhaps by using a system-level scheduler such as the Linux cron.

How to Optimize WordPress by Delegating WP-Cron to Linux Cron

In this section, first I’ll show you how to disable WP-Cron from being triggered on page load. Then I’ll share with you several commands you can use to run WP-Cron jobs manually, as well as how to hook up these commands to cron via a crontab file.

Toward the end of the article, we’ll go over some tips on how to verify your setup and some troubleshooting tricks that will come in handy should you run into any problems. Lastly, I will wrap up the article by briefly discussing what makes the best cron job frequency for WordPress.

Quick links

Stop WP-Cron from Running Automatically

First, stop WP-Cron from running automatically. This is done by adding the following line into wp-config.php:-

define('DISABLE_WP_CRON', true);

Prepare Empty Crontab

Forget about crontab -e, the best way to manage Linux cron jobs are through crontab files.

Create a new file for WP-Cron tasks under /etc/cron.d

$ cd /etc/cron.d
$ touch wp-cron

This wp-cron file (or whatever name you give it) will be used in the next section.

I will be referring to this file as “cron file” or “crontab” for cron table.

3 Ways to Delegate WP-Cron to Crontab

There are a few ways to execute WP-Cron tasks.

Here are three different methods you can use and the instructions on how to hook them up to cron.

1. Execute wp-cron.php Locally Using PHP

The first method is to run wp-cron.php locally by calling PHP directly.

You can run this command from your terminal to see if there’s any error. Just know that the command provides no response if there’s nothing wrong.

$ php /var/www/html/codedodle/wp-cron.php

In the cron configuration file we created in the previous section, add the following line.

* * * * * www-data php /var/www/html/wp-cron.php > /dev/null 2>&1

* * * * * means that the command is run every minute.

The next segment instructs Cron to run the job as user www-data.

php /var/www/html/wp-cron.php is the command to run. /var/www/html/ is the full path to your WordPress root.

/dev/null 2>&1 tells Linux to send output messages to oblivion (/dev/null) because we don’t care about the output message from the command if there’s any. More on this in the Troubleshooting section below.

If you’re hosting multiple WordPress websites, simply add another line to the file and change the paths. For example,

* * * * * www-data php /var/www/html/example.com/wp-cron.php > /dev/null 2>&1
* * * * * www-data php /var/www/html/example.org/wp-cron.php > /dev/null 2>&1
* * * * * www-data php /var/www/html/example.org/wp-cron.php > /dev/null 2>&1

And if cron is complaining about php not found, you can try explicitly setting the path to the PHP binary direct in the PATH variable, e.g.

PATH=/usr/bin
* * * * * www-data php /var/www/html/example.com/wp-cron.php > /dev/null 2>&1

Advantages

  • Fastest method of the three
  • Run locally – this means external access to wp-cron.php is no longer required and can be blocked off in the firewall to prevent malicious attacks.

Disadvantages

  • Security concern. Even though it’s run by user www-data, calling PHP on wp-cron.php directly might allow malicious users to access other locations on the server accessible by www-data. However, the risk level should be minimal.
  • Potential PHP version discrepancy. I.e. NGINX / Apache uses one PHP version but /usr/bin/php could be a different PHP version. However, this can be easily fixed by checking your setup properly. And it is unlikely to cause problems even if you don’t.
  • Cron file requires update if website paths change

2. Use WordPress Command-line Utility WP-CLI

For the second method, we’ll be using the official command-line utility from WordPress which is the WP-CLI.

First, have it installed on your server and verify that it’s working.

Next, use this command to run scheduled WordPress tasks manually:-

$ wp cron event run --due-now --path='/var/www/html'

--path='/var/www/html' specifies the full path to your WordPress installation. Without it you will have the cd to the WordPress path first which is not ideal for cron jobs.

Should you run into errors, here are some resources that might help you troubleshoot the errors:-

When you’re ready, set up the cron job by adding the following instructions in the the wp-cron file you created previously.

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

* * * * * www-data wp cron event run --due-now --path='/var/www/html' > /dev/null 2>&1 

First, PATH=/usr/bin:/usr/local/bin sets up the PATH environment variable for the jobs defined in this file.

The reason we need this is because by default, WP-CLI gets installed to /usr/local/bin which may not be in the default cron PATH.

Without it, you might get error like /bin/sh: 1: wp: not found.

* * * * * instructs cron to run it every minute.

www-data is the user that will run the job.

Then we have the WP-CLI command that we’ve discussed earlier. Followed by > /dev/null 2>&1 which hide the output messages.

Here’s an crontab example for multiple websites:-

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

* * * * * www-data wp cron event run --due-now --path='/var/www/website1.com' > /dev/null 2>&1 
* * * * * www-data wp cron event run --due-now --path='/var/www/website2.com' > /dev/null 2>&1 
* * * * * www-data wp cron event run --due-now --path='/var/www/website3.com' > /dev/null 2>&1 

Advantages

  • Fast
  • Run locally – this means you can prevent wp-cron.php getting malicious attacks by blocking it in the firewall.

Disadvantages

  • Dependencies: PHP and WP-CLI
  • Can’t change website paths without updating crontab.
  • Require more work to set it up
  • Security: unknown, but probably safer than or as safe as the PHP method. Unfortunately I don’t understand enough about the inner working of WP-CLI to comment on this.

3. Use wget to Access wp-cron.php via HTTPS

The last method involves calling wp-cron.php externally via HTTPS using programs such as wget or curl. I’ll be using wget in this example, but you can switch to curl and get similar results.

First, here’s how to do it manually.

$ wget --delete-after https://exampe.com/wp-cron.php?doing_wp_cron

--delete-after tells wget to delete the downloaded file at the end.

https://exampe.com/ is the URL of a WordPress site. wp-cron.php?doing_wp_cron is the WordPress endpoint and parameter for triggering and running scheduled jobs.

Example cron file:-

* * * * * www-data wget --delete-after https://example.com/wp-cron.php?doing_wp_cron <code>> /dev/null 2>&1
* * * * * www-data wget --delete-after https://example.org/wp-cron.php?doing_wp_cron > /dev/null 2>&1
* * * * * www-data wget --delete-after https://fakewebsite.com/wp-cron.php?doing_wp_cron > /dev/null 2>&1

Please refer to the previous section for the explanation of what * * * * *, www-data, and > /dev/null 2>&1 mean.

You should be able to call wget in crontab without specifying PATH and use absolute path, but if you run into any problems, check out the Troubleshooting section for some tips.

Advantages

  • Closest to how WP-Cron does it originally.
  • Used in official WordPress example.
  • Easiest to set up.
  • Probably the most secure of the three.
  • Machine independent – you can manage cron schedules from an external server.
  • Little to no dependencies – you don’t have to worry about PHP binary version discrepancy, WP-CLI getting out of date, or changing WordPress directory.

Disadvantages

  • Slower and require more resources compared to other methods–when you do it from the same machine that hosts the WordPress sites.
  • Ultimately, wp-cron.php is triggered externally – that means you can’t block external access to wp-cron.php in your firewall. Workaround: block access to wp-cron.php but whitelist the triggering server.

How to Verify Your WordPress Crontab Is Running

First, install a WordPress cron monitoring plugin. I have been using WP-crontrol and it has served me well.

wp crontrol wordpress cron monitoring plugin

The plugin shows you the jobs that are queued in the internal WP-Cron system, their frequencies (recurrence), and when is the next time they’re supposed to run.

Also pay attention to the top of the screen that says DISABLE_WP_CRON is set to true. That’s what you want to see. That means WordPress won’t be trying to trigger scheduled tasks on page load anymore.

If your crontab isn’t set up properly, WP-Crontrol will let you know by highlighting the tasks (events) that have missed their schedule:-

wp-cron missed schedule

Crontab Troubleshooting Tips

Here are some tips that will help you find the root cause of your cron-related problems.

1. Look at the System Log

Cron-related messages are recorded in /var/log/syslog

$ grep -i cron syslog

Output snippet:-

Aug 29 04:19:01 codedodle cron[25896]: (*system*wp-crontab) RELOAD (/etc/cron.d/wp-crontab)
Aug 29 04:19:01 codedodle CRON[38521]: (manager) CMD (wp cron event run --due-now --path='/var/www/html' > /dev/null 2>&1)
Aug 29 04:20:01 codedodle CRON[38528]: (manager) CMD (wp cron event run --due-now --path='/var/www/html' > /dev/null 2>&1)
Aug 29 04:21:01 codedodle CRON[38530]: (manager) CMD (wp cron event run --due-now --path='/var/www/html' > /dev/null 2>&1)

In the output above, the first line tells us that the custom crontab file we made is recognized by cron.

The second to the last line show that the command we defined in the crontab is being executed once every minute, as indicated by the timestamp.

This suggests that the problem might come from the command itself or the cron environment that is inappropriately set.

2. Log Command Output

/dev/null 2>&1 sends STERR to STDOUT then to the void. This is useful when are not interested in the output messages.

For debugging purposes, log the output to a file. For example,

* * * * * www-data wp cron event run --due-now --path='/var/www/html' >> /tmp/last-cron.log 2>&1

This instruction appends all output messages to /tmp/last-cron.log.

You can then troubleshoot further based on the information found in that file.

3. Show Cron Environment

Another trick I found useful is to add a command in the crontab to show the environment variables.

Use printenv to print the environment variables and save it in a log file:-

* * * * * www-data printenv >> /tmp/last-cron.log
* * * * * www-data wp cron event run --due-now --path='/var/www/html' >> /tmp/last-cron.log 2>&1

/tmp/last-cron.log:-

HOME=/var/www
LOGNAME=www-data
PATH=/usr/bin:/bin
LANG=C.UTF-8
SHELL=/bin/sh
PWD=/var/www

This will show you what kind of environment that cron daemon is using to run your commands.

4. The Most Common Error

In my experience, ill-defined paths usually account for 90% of cron errors.

There are two solutions to this problem.

One is to always use absolute path in a crontab.

Two is to explicitly define the PATH variable for your crontab.

For example, if you neither define PATH nor use absolute path for the WP-CLI method, you would have gotten this error:-

/bin/sh: 1: wp: not found

By calling printenv from a cron task (see previous point) we can see that this the PATH setting:-

PATH=/usr/bin:/bin

However, wp is located under /usr/local/bin which is not in PATH:-

$ which wp
/usr/local/bin/wp

One solution is to use absolute path in your cron instruction:-

* * * * * www-data /usr/local/bin/wp cron event run --due-now --path='/var/www/html' > /dev/null log 2>&1

The other solution is to add /usr/local/bin to PATH:-

PATH=/usr/local/bin:/usr/bin:/bin
* * * * * www-data wp cron event run --due-now --path='/var/www/html' > /dev/null log 2>&1

Optimal Cron Job Frequency for WordPress

The optimal frequency is as low as possible based on the requirements of your WordPress scheduled tasks.

For example, say this is what your WordPress events look like

wp-cron events or tasks

The most frequent event / task is wp_privacy_delete_old_export_files which is set to run once an hour. This suggests that you can set your crontab frequency to hourly without affecting anything.

However, this task doesn’t seem that critical. I mean “delete old export files” doesn’t seem too important to me. It’s probably OK for it to wait a few more hours.

On the other hand, recovery_mode_clean_expired_keys sounds like a critical task. Its frequency is once daily. And we might not want to delay it too much. This suggests that if you want to really optimize your cron jobs, you can instruct the Linux cron daemon to trigger WP-Cron once a day.

Just for your information, I have mine set to run every minute because that’s just the easiest and it works for all of my WordPress setups.