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.