#63858 closed enhancement (fixed)
Trigger the wp_cron action from the shutdown hook instead of the init hook to reduce TTFB (⏱️ Time To First Byte) and improve performance 📈
| Reported by: |
|
Owned by: |
|
|---|---|---|---|
| Milestone: | 6.9 | Priority: | normal |
| Severity: | normal | Version: | 2.1 |
| Component: | Cron API | Keywords: | has-patch needs-testing has-unit-tests has-dev-note |
| Focuses: | performance | Cc: |
Description
Spawn cron via HTTP API requires around 1 second, as discussed on https://core.trac.wordpress.org/ticket/63547. We should trigger the wp_cron action from the shutdown hook instead of the init hook to reduce TTFB (Time To First Byte) and improve performance
Reference: https://core.trac.wordpress.org/ticket/63547#comment:7
Change History (16)
This ticket was mentioned in PR #9574 on WordPress/wordpress-develop by @pmbaldha.
5 months ago
#1
- Keywords has-patch added
#2
@
5 months ago
I've reviewed the history of cron spawing.
- [3561] - moved from
shutdown_action_hook()toinit - [10521] - moved to
sanitize_comment_cookies - [20652] - moved back to
init
Unfortunately most of these commits are from a time before detailed commit messages so there's not a lot of context beyond the commit messages.
#4
@
3 months ago
- Milestone changed from Awaiting Review to 6.9
For reference: The shutdown action is triggered by shutdown_action_hook(), and shutdown_action_hook is added via register_shutdown_function( 'shutdown_action_hook' ).
Of particular note in the PHP docs for register_shutdown_function():
Shutdown functions run separately from the time tracked by max_execution_time. That means even if a process is terminated for running too long, shutdown functions will still be called. Additionally, if the max_execution_time runs out while a shutdown function is running it will not be terminated.
This would seem to address any concerns about whether slowness in wp_remote_post() noted in #63547 would end up preventing cron from being spawned.
Additionally, I just checked the order in which things happen and an output buffer callback (such as is proposed in #43258) will be executed before shutdown. This means the buffer can be flushed to the client without waiting potentially for a slow wp_remote_post(). In other words, even if the performance of wp_remote_post() is not improved, this shouldn't hold up the page from being served.
This being the case, we may then want to turn off the asynchronous nature of the cron spawning if it happens at shutdown specifically if we find that an async request doesn't always result in a successful spawn. But if we find that it is always spawned as expected, then no need to make it synchronous.
#5
@
3 months ago
In yet other words, even without #43258, moving cron to spawn at shutdown should improve TTFB even if TTLB (time to last byte) doesn't end up changing.
#6
@
3 months ago
- Keywords needs-testing added
- Version set to 2.1
The rationale for moving from shutdown to init is explained in r3561 as follows:
Move wp_cron from shutdown hook to init. It was acting all funky (in the bad way) in the shutdown hook.
It would be nice to know what was funky about this. This commit was 20 years ago as part of WordPress 2.1. As of that commit, spawn_cron() looked like this:
<?php function spawn_cron() { if (array_shift(array_keys(get_option('cron'))) > time()) return; $cron_url = get_settings('siteurl') . '/wp-cron.php'; $parts = parse_url($cron_url); $argyle = @ fsockopen($parts['host'], $_SERVER['SERVER_PORT'], $errno, $errstr, 0.01); if ( $argyle ) fputs($argyle, "GET {$parts['path']}?time=" . time() . '&check=' . md5(DB_PASS . '187425') . " HTTP/1.0\r\nHost: {$_SERVER['HTTP_HOST']}\r\n\r\n"); }
It could be that fsockopen() would fail during a shutdown at that time. Back then, the latest version of PHP was 5.1.2. But that's just a guess.
#7
@
3 months ago
- Keywords needs-dev-note added
@westonruter I've been considering this and think it could be worth trying if committed prior to the first beta release. It would be good to have a dev note published around the same time with a call out for testing.
Currently, the cron loopback request fires on the wp_loaded hook in the function _wp_cron(). wp_cron fires on the init hook and registers the loopback request to fire later. See the source code.
The wrapper function was added in WordPress 5.7.0 to maintain backward compatibility for plugins running remove_action( 'init', 'wp_cron' ).
In this case, I think changing the hook used in wp_cron is adequate.
#8
@
3 months ago
@peterwilsoncc OK, so would that mean this change then?
-
src/wp-includes/cron.php
a b function spawn_cron( $gmt_time = 0 ) { 985 985 * @since 2.1.0 986 986 * @since 5.1.0 Return value added to indicate success or failure. 987 987 * @since 5.7.0 Functionality moved to _wp_cron() to which this becomes a wrapper. 988 * @since 6.9.0 The `_wp_cron()` function is moved to the shutdown action to prevent the async loopback request from increasing TTFB. 988 989 * 989 * @return false|int|void On success an integer indicating number of events spawned (0 indicates no 990 * events needed to be spawned), false if spawning fails for one or more events or 991 * void if the function registered _wp_cron() to run on the action. 990 * @return void The function registered _wp_cron() to run on the shutdown action. 992 991 */ 993 992 function wp_cron() { 994 if ( did_action( 'wp_loaded' ) ) { 995 return _wp_cron(); 996 } 997 998 add_action( 'wp_loaded', '_wp_cron', 20 ); 993 add_action( 'shutdown', '_wp_cron' ); 999 994 } 1000 995 1001 996 /**
This ticket was mentioned in PR #10205 on WordPress/wordpress-develop by @westonruter.
3 months ago
#10
Trac ticket: https://core.trac.wordpress.org/ticket/63858
#11
@
3 months ago
- Focuses performance added
- Keywords has-unit-tests added
- Owner set to westonruter
- Status changed from new to accepted
I'm testing this new PR with the following plugin active:
<?php /** * Plugin Name: Cron TTFB Fix Test */ add_action( 'http_api_debug', static function ( $response, $context, $class, array $parsed_args, $url ) { if ( str_contains( $url, 'wp-cron.php' ) ) { error_log( 'Cron request spawned: ' . $url ); sleep( 1 ); } }, 10, 5 ); add_action( 'init', function () { if ( defined( 'DOING_CRON' ) ) { error_log( 'Doing cron!! Spawned URL: ' . $_SERVER['REQUEST_URI'] ); } } );
Note that it adds 1 second of additional latency on purpose to the wp_remote_post() call to simulate the HTTP API not being correctly handling the timeout and blocking params.
To get a baseline TTFB and TTLB for the homepage:
$ curl -o /dev/null -s -w 'TTFB: %{time_starttransfer}s\nTTLB: %{time_total}s\n' http://localhost:8000/
TTFB: 0.176569s
TTLB: 0.177882s
So the TTFB for a homepage response is ~177 milliseconds and a millisecond longer for the last byte.
Then I test spawning cron. On trunk I run the following from the command line:
npm run env:cli cron event schedule foo '+1 second';
sleep 2;
curl -o /dev/null -s -w 'TTFB: %{time_starttransfer}s\nTTLB: %{time_total}s\n' http://localhost:8000/
This results in:
Success: Scheduled event with hook 'foo' for 2025-10-10 00:13:59 GMT. TTFB: 1.210728s TTLB: 1.211613s
Note the TTFB is now 1 second worse because that HTTP request took longer, and the Time To Last Byte (TTLB) is almost identical to the TTFB.
The expected "Doing cron!!" message also appears in my error log.
Then I switch to the branch in the PR and re-run the above command:
Success: Scheduled event with hook 'foo' for 2025-10-10 00:14:43 GMT. TTFB: 0.185948s TTLB: 1.196698s
I again see “Doing cron!!” appear in the error log.
Note the TTFB is back down to the original range at 162 ms, even though the TTLB is unchanged.
All this indicates that cron is successfully moved to shutdown and that this improves TTFB.
Trac ticket: https://core.trac.wordpress.org/ticket/63858