How late do scheduled tasks run when visitors trigger them?
Whisk. Published 5 October 2026. Measurements taken 4 and 5 October 2026.
Abstract
Some web applications run their scheduled tasks only when someone visits the site. WordPress does this by default with WP-Cron [1], and Drupal does it with its Automated Cron module [2]. We measured how late such tasks run. Six identical WordPress 6.9.4 sites each had a task due every five minutes for 90 minutes, and each received simulated visitors at random at a different average rate, from none to 360 an hour. In all 72 cases where a due task ran, it ran within one second of the first visit after its due time, so the delay is set entirely by the gap between visits. Median delays were 6 s at 360 visits an hour, 43 s at 60 an hour and 190 s at 12 an hour, close to what a random-arrival model predicts (7 s, 42 s and 208 s). At 4 visits an hour, 9 of 18 tasks had not run by the end of the test. At one visit an hour, 17 of 18 had not. On the site with no visitors, no task ran until a single visit at 40 minutes, which ran all 8 overdue tasks at once.
1. Introduction
A web application often has work to do on a schedule: sending reminder emails, publishing posts set for a later time, clearing expired sessions, checking for updates. On a server the usual tool for this is cron, a system service that starts each task at its set time. Many web hosts, and many applications meant to install anywhere, cannot rely on cron being available, so some applications run their own scheduler inside ordinary page requests instead. This is known as visitor-based or "poor man's" cron.
WordPress documents that WP-Cron "works by checking, on every page load, a list of scheduled tasks to see what needs to be run," and that "scheduling errors could occur if you schedule a task for 2:00PM and no page loads occur until 5:00PM" [1]. Drupal's Automated Cron module "works by checking at the end of each Drupal request to see when cron last ran" and the documentation advises that "for low-traffic sites it can also be desirable to create a cron job" [2]. Both projects say that tasks run late when visits are few. Neither says how late. This paper measures it for WordPress at six levels of traffic.
2. How WP-Cron runs tasks
We read the WP-Cron code in WordPress 6.9.4 (wp-includes/cron.php). On every request, WordPress registers a check to run after the page has been sent (the shutdown action). The check looks for any scheduled event whose time has passed. If there is one, and no other cron run has started in the last 60 seconds (WP_CRON_LOCK_TIMEOUT), WordPress sends a non-blocking request to its own wp-cron.php, which runs every overdue event in turn. The visitor's page is not slowed, but nothing runs until a request arrives. Setting DISABLE_WP_CRON turns this off, and WordPress then recommends calling wp-cron.php from the system scheduler instead [3].
3. Method
Sites. Six WordPress 6.9.4 sites ran on PHP 8.3.31 under Apache, from the official wordpress:6-php8.3-apache container image, sharing one MariaDB 11 server. All six had the same configuration, a fresh installation with no other plugins.
Scheduled tasks. A must-use plugin (Appendix A) recorded the time whenever a test task ran. Using WP-CLI, we scheduled 18 single events on each site, due every 300 seconds from 5 to 90 minutes after the start (Appendix B). Each event carried its own due time, so the delay of each run is its recorded time minus its due time.
Visitors. A separate container per site requested the home page at random times, with gaps drawn from an exponential distribution (a Poisson process), at an average rate of 0, 1, 4, 12, 60 or 360 visits an hour (Appendix C). Each visit's time was logged. The visitor ran for 100 minutes, ten minutes past the last due time.
Interruption. The host machine restarted 39 minutes into the run. All sites and visitor containers stopped for about 70 seconds and were then restarted with their scheduled events intact. The visitor schedules resumed with new random seeds for the remaining time. During the restart, a single check request reached the site meant to have no visitors, at 40 minutes. We kept it in the results and report that site as having one visit, at 40 minutes.
4. Results
4.1 Tasks run at the next visit
In every one of the 72 runs we recorded, the task ran within one second of the first visit after its due time. The 60-second cron lock never delayed a task in our data. The delay of a visitor-triggered task is therefore the wait for the next visitor and nothing else.
4.2 Delay by traffic level
| Visits per hour (average) | Visits received | Tasks run (of 18) | Median delay | 90th percentile | Longest delay |
|---|---|---|---|---|---|
| 360 | 575 | 18 | 6 s | 24 s | 47 s |
| 60 | 107 | 18 | 43 s | 112 s | 329 s |
| 12 | 24 | 18 | 190 s | 458 s | 533 s |
| 4 | 7 | 9 | 298 s (of those run) | n/a | 1,398 s, and 9 still waiting |
| 1 | 1 | 1 | 247 s (of the one run) | n/a | 17 still waiting |
| 0 (one visit at 40 min) | 1 | 8 | all 8 ran at that visit | n/a | 2,107 s, and 10 still waiting |
Delay is the time from a task's due time to when it ran. "Still waiting" tasks had not run when the test ended; each had waited between 10 and 90 minutes by then.
At 4 visits an hour, all seven visits fell in the first 49 minutes, and the 9 tasks due after the last visit were still waiting 10 to 50 minutes later when the test ended. At one visit an hour, the site received a single visit in 100 minutes. These are small samples, so Section 4.3 gives the expected delays for the low rates.
4.3 Expected delay
Because each task runs at the first visit after its due time, and visits arriving at random have exponentially distributed gaps, the expected delay at an average of r visits an hour has a median of ln 2 / r hours and a mean of 1 / r hours. At the three rates where every task ran, the measured medians were close to the model:
| Visits per hour | Model median | Measured median |
|---|---|---|
| 360 | 7 s | 6 s |
| 60 | 42 s | 43 s |
| 12 | 208 s | 190 s |
The same model gives the rates we could not sample well:
| Visits per hour | Median delay | Mean delay | Chance a task waits over 10 minutes | Chance it waits over an hour |
|---|---|---|---|---|
| 4 | 10 min | 15 min | 51% | 2% |
| 1 | 42 min | 60 min | 85% | 37% |
Real traffic is not uniform. Visits cluster in working hours and fall away at night and weekends, so a low-traffic site will see much longer delays overnight than these averages suggest.
4.4 Tasks run together after a gap
On the site with no visitors, none of the 8 tasks due in the first 40 minutes ran. The single visit at 40 minutes ran all 8 within 7 milliseconds of each other. The 10 tasks due afterwards never ran. A visitor-triggered scheduler does not lose tasks, but after a quiet period it runs every overdue task at once, in one request, whatever their original spacing.
5. Discussion
For a site with steady traffic of one visit a minute or more, WP-Cron's delays were under two minutes nine times out of ten, which suits tasks such as publishing a scheduled post. For a business site with a few visits an hour, which describes many company websites and internal tools, a task due at a given time will usually run tens of minutes late, sometimes hours late, and overnight not until the next morning's first visitor. Reminder emails, scheduled reports and anything with a deadline should not depend on it.
Two side effects matter as well. First, tasks that run in a batch after a quiet spell can arrive at an external service together, for example several emails sent in the same second. Second, the work happens in a request triggered by a visitor. WP-Cron sends it to a separate background request so the visitor's page is not slowed, but it still uses the web server's capacity at that moment.
The fix both projects recommend is to stop relying on visits: disable the visitor trigger and call the scheduler on a fixed timetable from the system's cron or from a hosting platform's scheduled jobs [2][3]. Delays then come down to the timetable's interval.
6. Limitations
We measured WordPress only. Drupal's Automated Cron works on the same principle but by default runs about every three hours, more or less often depending on traffic [2], so its delays are likely to be longer than those here. We tested single events. Recurring events are rescheduled each time they run and were not measured. Visitors arrived at random at a constant average rate, while real traffic varies through the day. Each traffic level had one 100-minute run of 18 tasks, so the results at the lowest rates are few. Section 4.3 gives model figures for those, and the model matched the measurements wherever we had enough data. The run was interrupted for about 70 seconds by a restart of the host machine, as described in Section 3.
7. Conclusion
A task scheduled with WordPress's default WP-Cron runs at the first page visit after it falls due, so its delay is the wait for that visit. On a site with a visit every ten seconds, the median delay was 6 seconds. With a visit every five minutes, it was about three minutes, and with a few visits an hour it is typically tens of minutes, with tasks piling up and running together after quiet spells. Sites that need tasks on time should run their scheduler from a real timer.
For related measurements, see our papers on files in an in-memory /tmp and container memory limits and on where the MP4 index sits and how that affects playback.
References
- WordPress. "Cron." Plugin Handbook, WordPress Developer Resources. https://developer.wordpress.org/plugins/cron/ (accessed 4 October 2026).
- Drupal. "Cron automated tasks overview." Drupal documentation. https://www.drupal.org/docs/administering-a-drupal-site/cron-automated-tasks/cron-automated-tasks-overview (accessed 4 October 2026).
- WordPress. "Hooking WP-Cron Into the System Task Scheduler." Plugin Handbook, WordPress Developer Resources. https://developer.wordpress.org/plugins/cron/hooking-wp-cron-into-the-system-task-scheduler/ (accessed 4 October 2026).
Appendix A: Recording plugin
Saved as wp-content/mu-plugins/probe.php. Each run appends the task's due time, the time it ran and the difference.
<?php
add_action('webcron_probe', function ($due) {
$now = microtime(true);
file_put_contents('/var/www/html/wp-content/probe.log',
sprintf("%d,%.3f,%.3f\n", $due, $now, $now - $due), FILE_APPEND | LOCK_EX);
});Appendix B: Scheduling the tasks
Run with PROBE_START=<unix time> PROBE_COUNT=18 wp eval-file schedule.php.
<?php
$start = (int) getenv('PROBE_START');
$count = (int) getenv('PROBE_COUNT');
for ($i = 1; $i <= $count; $i++) {
$due = $start + $i * 300;
wp_schedule_single_event($due, 'webcron_probe', [$due]);
}Appendix C: Simulated visitors
Run with python3 traffic.py http://site/ <visits per hour> <seconds> <seed> <name>.
import random, sys, time, urllib.request
url, rate_per_hour, seconds, seed = sys.argv[1], float(sys.argv[2]), int(sys.argv[3]), int(sys.argv[4])
rng = random.Random(seed)
end = time.time() + seconds
log = open(f"/out/{sys.argv[5]}.visits", "a")
while rate_per_hour > 0:
time.sleep(rng.expovariate(rate_per_hour / 3600))
if time.time() > end: break
try:
urllib.request.urlopen(url, timeout=30).read()
log.write(f"{time.time():.3f}\n"); log.flush()
except Exception as e:
log.write(f"{time.time():.3f},error,{e}\n"); log.flush()