A transient is a cache you own. You store a value with an expiry and read it back later. With a persistent object cache it lives in Redis; without one it lands in wp_options.
$data = get_transient( 'acme_top' );
if ( false === $data ) {
$data = acme_query_top_products();
set_transient( 'acme_top', $data, HOUR_IN_SECONDS );
}
That part is easy. The hard question comes next — when should this value be thrown away? The moment you add a cache you have taken on an invalidation problem, and that problem never shows up in the storing code. It shows up in production.
Two events that must invalidate
Every cache has at least two invalidation events. Name them explicitly or you will miss one.
Miss the first and an editor changes something while visitors keep seeing the old value until expiry. The more the value matters — price, stock, an announcement — the longer that window feels.
Miss the second and it is worse. Newly deployed code reads an array in the old shape, references a key that is not there, and throws a warning or a fatal. Locally the cache is empty so everything is fine; it only breaks in production.
A generation counter covers both
Once keys multiply across parameter combinations, deleting them individually stops being feasible. Put a generation number in the key and simply increment it: the old keys are never read again and quietly expire.
function acme_gen(): int {
return (int) get_option( 'acme_cache_gen', 1 );
}
function acme_top(): array {
$key = 'acme_top_v2_' . acme_gen(); // v2 = the structure version (event 2)
$data = get_transient( $key );
if ( false === $data ) {
$data = acme_query_top_products();
set_transient( $key, $data, HOUR_IN_SECONDS );
}
return $data;
}
// event 1 — the source changed, so move the generation on
add_action( 'save_post_product', 'acme_bump_gen' );
add_action( 'deleted_post', 'acme_bump_gen' );
function acme_bump_gen(): void {
update_option( 'acme_cache_gen', acme_gen() + 1 );
}
The v2 in the key handles the second event. Bump that string in any deploy that changes the stored structure and the new code never even looks at the old shape. It is safer than adding “flush the cache” to a deployment checklist, because it does not depend on somebody remembering.
A generation counter is one small integer, so carrying it on every request is harmless. It is large values that cause trouble, and that story returns two parts from now.
Stampedes and key length
When a popular entry expires, every request that arrives in that instant starts regenerating it at once. If the computation is heavy, that moment is your slowest. Mitigate it with a short lock value marking regeneration in progress, or by refreshing slightly before expiry.
Transient names also have a length limit. Concatenating parameters straight into the name gets silently truncated, so different conditions end up sharing a key. Fold the parameters into a hash instead — 'acme_top_v2_' . acme_gen() . '_' . md5( wp_json_encode( $args ) ).
Further articles on cache design sit in the performance archive, and some of these checks are surfaced by the site diagnostic among our free tools.
Next part
Everything so far has been a cache inside the application. A page cache, which stores the finished HTML whole, belongs somewhere else — next we look at why that layer lives on the server, and what goes wrong when you stack two of them.