Taking Control of the WordPress Scheduler — The Basics

July 7, 2016

The WordPress scheduler is a tricky beast. ‘WP-Cron‘ as configured by default can alternately fail spectacularly with a ‘missed schedule’ error on a post you were depending on publishing at a specific time, or slow down your server to a crawl. On the other hand, with proper configuration on the server end and clever development in your plugins, the scheduler can be really powerful.

Set up WP-Cron Right

Lucas Rolff wrote a terrific article called ‘Why WP-Cron sucks‘, and he gives an excellent explanation of how the basic WP-Cron mechanism works, its shortcomings, and some tips for setting up your server correctly. I recommend reading it, but I’ll summarize the most important suggestions for setup.

Replace WP-Cron with a real crontab

The details can differ slightly depending on your website hosting, but on a standard UNIX hosting, if you have terminal/SSH access, type ‘crontab -e’ to get into the crontab editor, and enter a line like this:

* * * * * wget -q -O - http://yourwebsite.com/wp-cron.php?doing_wp_cron >/dev/null 2>&1

Then save the file with :wq (colon w q enter) and you should see a message ‘new crontab installed’.

Some notes:

  • Most tutorials will give you examples of cron jobs running every 5 minutes, 15 minutes, or even once every hour. The crontab entry I listed above will run WP-Cron once every minute, though. I chose that to make sure that if you schedule something to happen at 6:01pm, you don’t have to wait until 6:05pm or 6:15pm (or 7pm!) for it to happen. If your website can’t handle one visit per minute, you have much more serious problems to deal with.
  • Of course ‘yourwebsite.com’ should be replaced with the actual domain name of your WordPress site.
  • If you hit http://(whatever-your-domain-is)/wp-cron.php?doing_wp_cron in a browser, it should show you a completely blank white screen. If you see an error page, then you aren’t pointing to the right URL. wp-cron.php is in the root of your WordPress installation, just like wp-admin. So if your wp-admin directory is http://www.somedomain.com/wordpress/wp-admin, wp-cron will be at http://www.somedomain.com/wordpress/wp-cron.php.
  • If you still aren’t getting expected results from manually hitting the wp-cron.php URL in a browser, it is probably because you’re running WP Multisite, or have some caching or proxy rules getting in the way. There are many possibilities and you’ll need to search Google or StackExchange for the right fix for you.

Disable WP-Cron in wp-config.php

This is included in all of the articles about replacing WP-Cron, but I recommend that you shouldn’t do it until after you’ve setup the crontab in the previous step. Fortunately, it is the same on all platforms.

define('DISABLE_WP_CRON', true);

Before you add that line to wp-config.php, WP-Cron will run every single time any page in your WordPress site loads. Running WP-Cron is fairly complex and involves looking stuff up in the database, so ideally we don’t want to run it on every pageview.

On the other hand, WP-Cron only runs if you get a page view, so if you schedule something to happen at 3AM, but nobody visited your site between 2AM and 7AM, that scheduled event would not happen until 7AM. Worse yet, if you schedule a post to publish at a specific time but WP-Cron isn’t run until maybe 15 minutes later, the post won’t publish — you’ll see ‘missed schedule’ in the admin next to the post title, and you’ll have to go in and manually publish the post, hours after you had been depending on it to be up!

Another issue we’ve seen with disabling WP-Cron without putting in a crontab to trigger it is that some plugins ‘stack up’ scheduled jobs. For instance, Disqus and WP-SEO will add entries to the overall ‘cron’ entry in WordPress’s options table when a post has been updated. If you have a site with lots of posts and WP-Cron disabled, and somehow the wp-cron.php URL gets hit manually, all of those stacked up scheduled jobs will start running. It can be enough to slow a site to a crawl while they clear out.

So first setup your ‘real’ crontab, THEN disable WP Cron.

Set up a daily schedule in your plugin

Now that you have the scheduler working reliably for your server, lets schedule doing something every day.

I’ve got a Custom Post Type where I hide content snippets and include them into posts and sidebars. Each snippet is its own ‘post’, but I’ve setup the CPT so you can’t hit the snippet directly as a post. I can schedule these posts to publish automatically on a schedule, but I’d also like to schedule them to become ‘private’ and hidden. I’ve setup a postmeta field ‘snippet_expire_datetime’ that holds that datetime string.

To keep this simple, I’ll schedule a daily job that finds any post with that ‘snippet_expire_datetime’ postmeta field, check the value against now(), and mark the post ‘private’ if it is going to expire in the next 24 hours.

To make this work, we need to do the following:

  1. Trigger a function I’ve named ‘snippets_setup_schedule()’ that’ll setup a custom schedule:
    add_action( 'wp', 'snippets_setup_schedule' );

    Yes, the function gets run on every page load.

  2. In that function, setup a scheduled event with a unique named hook. I’ve given the hook the name ‘snippetsdailyevent’.
    function snippets_setup_schedule() {
      if ( ! wp_next_scheduled( 'snippetsdailyevent' ) ) {
        // only schedule the event if it hasn't already been scheduled
        // note that 'daily' is built-in as an interval
        wp_schedule_event( time(), 'daily', 'snippetsdailyevent');

    Since wp_next_scheduled is just checking the value of an auto-loaded option, it is quite lightweight, so running it on every page load should have minimal performance impacts.

  3. When the event with that hook happens, trigger a function I’ve named ‘snippets_daily_expire()’ that expires posts:
    add_action( 'snippetsdailyevent', 'snippets_daily_expire' );

Here it is all together, combined with the example function we want to run daily.

// this is the function we're running once a day. 
// Its garbage, but useful for an example  
function snippets_daily_expire() {
  $expire_snippets_before_this_date = strtotime("+1 day", time());
  $args = array(
    'post_status' => 'publish', 'post_type' => 'snippets',
    'meta_query' => array(
      array('key' => 'snippet_expire_datetime', 'value' => '', 'compare' => '!=')
    'posts_per_page' => -1, 'orderby' => 'date', 'order' => 'DESC'
  $snippetposts = new WP_Query($args);
  if ($snippetposts->have_posts()){
    while ( $snippetposts->have_posts() ) : $snippetposts->the_post();
      $expiredate = get_post_meta($snippetposts->post->ID, 'snippet_expire_datetime', true);
      if ($expiredate) {
        $expiredate_unixtime = strtotime($expiredate);
        if ($expiredate_unixtime < $expire_snippets_before_this_date && $expiredate_unixtime > 0){
          $success = wp_update_post( 'ID' => $snippetposts->post->ID, 'post_status' => 'private' );	
// whenever our scheduled event occurs, run the above function
add_action( 'snippetsdailyevent', 'snippets_daily_expire' );

// schedule our daily event
function snippets_setup_schedule() {
  if ( ! wp_next_scheduled( 'snippetsdailyevent' ) ) {
    // only schedule the event if it hasn't already been scheduled
    wp_schedule_event( time(), 'daily', 'snippetsdailyevent');
// run this function on every load to make sure the event is scheduled
add_action( 'wp', 'snippets_setup_schedule' );

A few things worth noting —

  • In our examples so far, this daily event will be scheduled to happen at the time of day the plugin was first activated. We’ll get into selecting a specific time to run things at in our next post.
  • wp_schedule_event has built-in options for recurrence of ‘hourly’, ‘twicedaily’, and ‘daily’ — the default is null aka no recurrence.
  • Setting up your own recurrence interval is also an option by plugging into the ‘cron_schedules‘ filter. Want to setup a job that runs every five minutes?
    add_filter( 'cron_schedules', 'cron_add_fiveminute' );
    function cron_add_fiveminute( $schedules ) {
      // interval below is in seconds
      $schedules['fiveminute'] = array(
        'interval' => 300,
        'display' => __( 'Every Five Minutes ' )
      return $schedules;

    Now wp_schedule_event will accept ‘fiveminute’ as a recurrence value!

  • Clean up after yourself! Assuming you’re building this in a plugin, use register_deactivation_hook and remove the recurring event from WordPress using wp_clear_scheduled_hook, eg
    function snippetplugin_deactivation() {
    register_deactivation_hook( __FILE__, 'snippetplugin_deactivation' );
  • There’s an excellent tool at https://generatewp.com/schedule_event/ that generates code to create simple scheduler events for your theme or plugin. It has a wizard and works quite well.

What next?

We’ve got the basics of the scheduler setup. Our environment should be checking cron every minute, and we can setup an automated job. But there’s much more to cover — taking control of just exactly when we want our job to run, timezone complexities, and even using cron to figure out when to run other cron jobs! So watch this space!

Digital at The WNET Group is not responsible for your or any third party’s use of code from this tutorial. All the information on this website is published in good faith “as is” and for general information purposes only; WNET and IEG make no representation or warranty regarding reliability, accuracy, or completeness of the content on this website, and any use you make of this code is strictly at your own risk.