woocommerce Case study
Transient Storm Guard
Two plugins on the store write a transient per product view: WooCommerce related products and YITH recently viewed. On a catalog this size that becomes tens of thousands of rows in wp_options, and every WordPress bootstrap pays for it. This is the guard that keeps the growth bounded.
The business problem
wp_options was carrying tens of thousands of _transient_wc_related_ and _transient_yith_wrvp_ rows. The usual advice is a nightly cleanup cron, but a blanket DELETE JOIN against a table that size holds it long enough to be felt on the front end. I wanted cleanup that fires only when there is actually a storm, and that never holds the options table for long.
What I delivered
- mu-transient-storm-guard.php, a 102-line MU plugin that runs one daily event and does nothing at all on a healthy day.
- Per-family thresholds instead of a single global one: wc_related is purged above 4,000 rows, yith_wrvp above 2,000, because the two families grow at different rates.
- A chunked expired-transient sweeper that deletes 200 timeout rows per run rather than the DELETE JOIN most cleanup snippets reach for.
- A guard that skips the entire run during WP All Import jobs, so a catalog import never competes with cleanup for the same table.
Technical approach
- Count first, delete second. Each family is measured with a COUNT(*) and only purged if it crosses its own threshold, which makes the plugin a no-op most days.
- Purges are capped at 2,000 rows and always remove the value row together with its matching _timeout_ row, so no orphan timeouts survive to be counted again tomorrow.
- The daily event is scheduled 300 seconds after init rather than immediately, keeping it away from the moment the site is already busy booting.
- Multisite gets its own pass through the sweeper with the network flag set.
Result and evidence
The two storm families stay under their thresholds and wp_options stopped being a moving target. I did not benchmark bootstrap time before and after under controlled conditions, so the claim I will make is structural: on an ordinary day cleanup now costs one counting query per family, and never a table-wide join.
Commercial value
This is maintenance that is invisible when it works. About a hundred lines removed a recurring source of unexplained slowness that had been coming back every few weeks.
Readable implementation brief
implementation_brief {
project: "Transient Storm Guard"
file: "mu-plugins/mu-transient-storm-guard.php (102 lines)"
trigger: "daily cron, scheduled init + 300s"
thresholds: "wc_related > 4000 rows, yith_wrvp > 2000 rows"
purge: "capped at 2000 per family, value + timeout rows"
sweep: "200 expired transients per run, no DELETE JOIN"
skips: "WP All Import requests, to avoid table contention"
}What this project shows
The decision worth noticing here is the threshold, not the delete. Anyone can write a cleanup cron; the risk is that the cleanup becomes the problem on a busy table.
I would rather ship something that usually does nothing and only acts when the numbers justify it. Measure, then intervene narrowly, is how I approach most operational work.