I have a self-hosted healthchecks.io instance (mentioned), and I use it to make sure all the needful cron jobs for my site actually run. I have it installed via Docker, so it’s not super complex to update and that’s how I like it.
The first cron jobs I monitored were the ones I have setup in my crontab
on the server:
- Run WP ‘due now’
- Set daily random ‘of the day’
- Download an iCal file
- Run a nightly data validity check
I used to have these using WP Cron, but it’s a little too erratic for my needs. This is important, remember this for later, it’ll come back up.
Once I added in those jobs, I got to thinking about the myriad WP Cron jobs that WordPress sets up on its own.
In fact, I have a lot of them:
+------------------------------------------------+---------------------+-----------------------+---------------+
| hook | next_run_gmt | next_run_relative | recurrence |
+------------------------------------------------+---------------------+-----------------------+---------------+
| rediscache_discard_metrics | 2025-04-25 17:51:15 | now | 1 hour |
| wp_privacy_delete_old_export_files | 2025-04-25 18:16:33 | 20 minutes 38 seconds | 1 hour |
| wp_update_user_counts | 2025-04-25 20:30:03 | 2 hours 34 minutes | 12 hours |
| recovery_mode_clean_expired_keys | 2025-04-25 22:00:01 | 4 hours 4 minutes | 1 day |
| wp_update_themes | 2025-04-26 04:57:57 | 11 hours 2 minutes | 12 hours |
| wp_update_plugins | 2025-04-26 04:57:57 | 11 hours 2 minutes | 12 hours |
| wp_version_check | 2025-04-26 04:57:57 | 11 hours 2 minutes | 12 hours |
[...]
+------------------------------------------------+---------------------+-----------------------+---------------+
While I could manually add them all to my tracker, the question comes up with how to add the ping to the end of the command?
The Code
I’m not going to break down the code here, it’s far too long and a lot of it is dependant on my specific setup.
In essence, what you need to do is:
- Hook into
schedule_event
- If the event isn’t recurring, just run it
- If it is recurring, see if there’s already a ping check for that event
- If there’s no check, add it
- Now add the ping to the end of the actual cron even
- Run the event
I actually built out code like that using Laravel recently, for a work related project, so I had the structure already in my head and I was familiar with it. The problem though is WP Cron is nothing like ‘real’ cron.
Note: If you really want to see the code, the beta code can be found in the LWTV GitHub repository. It has an issue with getting the recurrence, which is why I made this post.
When CRON isn’t CRON
From WikiPedia:
The actions of cron are driven by a crontab (cron table) file, a configuration file that specifies shell commands to run periodically on a given schedule. The crontab files are stored where the lists of jobs and other instructions to the cron daemon are kept.
Which means crontab runs on the server time. When the server hits the time, it runs the job. Adding in jobs with the ping URL is quick:
*/10 * * * * /usr/bin/wp cron event run --due-now --path=/home/username/html/ && curl -fsS -m 10 --retry 5 -o /dev/null https://health.ipstenu.com/ping/APIKEY/due-now-every-10
This job relies on the server being up and available, so it’s a decent metric. It always runs every ten minutes.
But WP Cron? The ‘next run’ time (GMT) is weirdly more precise, but less reliable. 2025-04-25 17:51:15
doesn’t mean it’ll run at 5:51pm GMT and 15 seconds. It means that the next time after that timestamp, it will attempt to run the command.
Since I have a scheduled ‘due now’ caller every ten minutes, if no one visits the site at 5:52pm (rounding up), then it won’t run until 6pm. That’s generally fine, but HealthChecks.io doesn’t really understand that. More to the point, I’m guestimating when
HealthChecks.io has three ways to check time: Simple, Cron, and onCalendar. In general, I use Cron because while it’s cryptic, I understand it. That said, there’s no decent library to convert seconds (which is what WP uses to store the interval timing) which means you end up with a mess of if checks.
A Mess of Checks
First, pick a decent ‘default’ (I picked every hour).
- If the interval in seconds is not a multiple of 60, use the default.
- If the interval is less than 60 seconds, run every minute.
- Divide seconds by 60 to get minutes.
- If the interval in minutes is not a multiple of 60, use the default.
- If the interval is less than an hour (1 to 59 minutes), run every x minutes.
- Divide minutes by 60 to get hours.
- If the interval in hours is not an even number of days (divide hours by 24), use the default
- If the interval is less than a day (1 to 23 hours), run every X hours.
- Divide hours by 24 to get days.
- If the days interval is not a multiple of 7 , use the default.
- If the interval is less than a week (1 to 6 days), run every X days.
- Divide days by 7 to get weeks.
- If the interval is a week, run every week on ‘today’ at 00:00
You see where this is going.
And then there’s the worse part. After you’ve done all this, you have to tweak it.
Tweaking Timing
Why do I have to tweak it? Well for example, let’s look at the check for expired transients:
if ( ! wp_next_scheduled( 'delete_expired_transients' ) && ! wp_installing() ) {
wp_schedule_event( time(), 'daily', 'delete_expired_transients' );
}
This runs every day. Okay, but I don’t know exactly when it’ll run, just that I expect it to run daily. Using my logic above, the cron time would be 0 0 * * *
which means … every day at midnight server time.
But, like I said, I don’t actually know if it’ll run at midnight. In fact, it probably won’t! So I have to setup a grace period. Since I don’t know when in 24 hours something will run, I set it to 2.5 times the interval. If the interval runs every day, then I consider it a fail if it doesn’t run every two days and change.
I really hate that, but it’s the best workaround I have at the moment.
Should You Do This?
Honestly?
No.
It’s positively ridiculous to have done in the first place, and I consider it more of a Proof of Concept than anything else. With the way WP handles cron and scheduling, too, it’s just a total pain in the backside to make this work without triggering alerts all the time!
But at the same time, it does give you a lot more insight into what your site is doing, and when it’s not doing what it should be doing! In fact, this is how I found out that my Redis cache had held on to cron jobs from plugins long since removed!
There are benefits, but most of the time this is nothing anyone needs.
Comments
One response to “Automate Your Site Checks with Cron (and WPCron)”
Apparently ActivityPub changed things π All fixed for next time!