A transient with an expiry of zero never expires

Passing 0 as the expiry reads like “expire immediately” and means the opposite: no expiry is stored, so the transient lives until something deletes it. A variable that is unexpectedly zero therefore makes a cache permanent.

set_transient( 'catalogue_counts', $data, $ttl );   // $ttl = 0 → forever

// and without a persistent object cache it lands in wp_options
// as two rows: the value and _transient_timeout_*

Expired transients are only cleaned up when something asks for them, so a site writing many short-lived transients under keys that are never read again accumulates rows indefinitely — a common reason for a wp_options table with a hundred thousand rows. With a persistent object cache they never touch the database at all, which also means they vanish on a cache flush and the code must cope with that.