Transients without an object cache live in options forever

A transient with an expiry is stored as two rows in the options table, and nothing deletes an expired one until something asks for it.

SELECT COUNT(*) FROM wp_options
WHERE option_name LIKE '_transient_timeout_%'
  AND option_value < UNIX_TIMESTAMP();
-- 214,008

DELETE a, b FROM wp_options a
LEFT JOIN wp_options b ON b.option_name =
  CONCAT('_transient_', SUBSTRING(a.option_name, 20))
WHERE a.option_name LIKE '_transient_timeout_%'
  AND a.option_value < UNIX_TIMESTAMP();

Expired transients are only removed when get_transient is called for that exact key, so a transient keyed on something transient — a search term, a product id — accumulates without limit. With a persistent object cache the whole mechanism bypasses the database and expires properly, which is the real fix. Without one, a scheduled cleanup is necessary and the site should be treated as having a leak rather than a cache.