Do the slow work after the page is sent: WordPress’s shutdown hook

Last month I added a search log to my WordPress plugin: every time the AI layer surfaced a product that plain keyword matching had missed, I wanted a row in a table saying so. Useful feature. Also a great way to make every single search on someone’s store slower, if you write it the obvious way.

The fix is a WordPress hook that does not get talked about enough: shutdown.

The problem with the obvious version

You are already inside the search filter, you have the data, so you write the row there:

It works. It is also bookkeeping that the shopper is paying for with their time. One INSERT is a millisecond or two, which sounds like nothing until you remember that this runs on every search, that the database might be on another host, and that your “one small write” has a way of becoming three writes and a lookup six months later.

The visitor needs the search results. They do not need your analytics.

What the shutdown hook actually is

shutdown is the last action WordPress fires. Core registers it as a PHP shutdown function, so it runs at the very end of the request – after the template has rendered, after the response has been handed off. It fires on normal page loads, on AJAX, on REST requests, and even when something calls wp_die().

That makes it the natural home for work that must happen during a request but that nobody is waiting for.

Why it feels instant: fastcgi_finish_request()

On PHP-FPM – which is what most decent hosting runs – PHP can tell the web server “I am done, send it” and keep executing. WordPress core and various plugins call fastcgi_finish_request() at the end of a request, so by the time your shutdown callback runs, the browser has already received the page and started rendering it.

Be honest with yourself about what that means, though. The visitor is not waiting, but the PHP worker still is. You have moved the cost off the perceived page load and onto your pool of workers. That is a very good trade for a single INSERT. It is a terrible trade for something that takes two seconds, because under traffic you will run out of workers and the queue will bite you in a much less obvious way.

On old mod_php or CGI setups the connection may not be closed early at all, so the technique degrades to “same total time, tidier code”. Nothing breaks – it just stops being a performance win.

The pattern, in three moves

This is the actual code from AI Search 1.31.0, trimmed for reading. First, register the hook where you register the others:

Second, during the request you only collect. No queries, no writes – just put what you know on the object and carry on:

Third, the callback that actually writes. Note the early return and the null: this thing must be safe to call when there is nothing to do, and safe to call twice.

That is the whole trick. The search path does zero database work for logging; the row still lands on every relevant request; and the shopper never waits for it.

When you should not reach for it

  • Anything the response depends on. Obvious when written down, easy to forget at 11pm. If the page needs the result, it cannot happen after the page.
  • Anything slow. Calling an external API, resizing images, sending mail through a slow SMTP server – all of these hold a worker hostage. That is what WP-Cron, or a real queue, is for.
  • Anything that needs to output. Headers are long gone. No redirects, no cookies, no printing. If you echo something here you will either see it glued to the end of your page or nowhere at all.
  • Anything that must not be lost. A fatal error, a killed worker or an aggressive timeout can end the request before your callback runs. For a search log that is fine. For an order, it is not.

Small gotchas worth knowing

$wpdb is still perfectly alive at this point, so queries work normally. Object cache writes, on the other hand, are mostly pointless: nothing else in this request will read them. And if you use a persistent object cache, remember that other requests may already be serving cached output that does not know about your write – so do not use shutdown to fix up something the current page displayed incorrectly.

Also, keep the callback idempotent. In unusual flows (some AJAX handlers, some error paths) you can end up with the shutdown sequence running more than once. Nulling the pending payload after reading it, as above, is the cheapest possible insurance.

The result in practice

In AI Search this powers the Insights tab: store owners can see which searches the AI actually rescued – the queries where semantic matching surfaced products that keyword matching had missed – without the logging costing their shoppers anything. Same feature, same data, moved a few milliseconds to the right in the request lifecycle.

It is a small technique. But “does this actually need to block the response?” is a question worth asking about half the code in a typical plugin, and shutdown is the tool for the times when the answer is no.

Want a second pair of eyes on your site?

I review WordPress and WooCommerce sites for performance, search quality and the small architectural decisions that quietly cost you speed. If you would like to know where yours can improve, let’s talk.


Posted

in

, , , ,

by

Tags:

Comments

Leave a Reply

Your email address will not be published. Required fields are marked *