Technote

Performance optimization Practical

Series Profiling WordPress performance Part 5 of 8

Transients: caching hands you an invalidation problem

Storing takes three lines. Deciding when to delete is the hard part, and there are at least two events. Miss either one and the symptoms differ sharply.

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.

Two events and one safety net — using expiry as the strategy means lying for that long

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.

More on this topic

All technotes

Performance optimization Practical

A landing page speed audit you can run yourself

Asking engineering to "make it faster" gives nobody a task. Images, third-party scripts, fonts and the hero image are four things a marketer can check personally and hand…

Marketers 9 min read

₩270,000 · Join the program