Amirali YaghoutiSenior Software Engineer

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.

implementation-brief.readme

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.