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 over a million 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 about 1.3 million _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 for visitors to feel it. 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. I measure each family with a COUNT(*) and purge it only when it crosses its own threshold, which makes the plugin a no-op on most days.
  • I cap each purge at 2,000 rows and always delete the value row together with its matching _timeout_ row, so no orphan timeouts survive to be counted again tomorrow.
  • I schedule the daily event 300 seconds after init rather than immediately, which keeps 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: the transient table went from about 1.3 million rows to about 180 thousand, the figure recorded in the public performance notes. I did not benchmark bootstrap time before and after under controlled conditions, so on time I only make the structural claim. 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 threshold is the part worth looking at here, not the delete. Anyone can write a cleanup cron; the risk is that the cleanup itself 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.