What Actually Happens When wp_cache_flush() Clears the alloptions Key Mid-Request

The report usually arrives in the same shape: a request calls wp_cache_flush(), and immediately afterward the options table gets hammered. Query Monitor shows a cluster of SELECT option_name, option_value FROM wp_options WHERE autoload IN (...) rows, the alloptions cache miss counter climbs, and someone on the team concludes that wp_cache_flush() deleted the alloptions key mid-request and triggered a stampede.

That conclusion is half right and half wrong, and the wrong half matters because it sends you looking for a key deletion that core does not perform. Core’s flush is broader and blunter than a targeted delete. The stampede risk is real, but it lives in the repopulation path, not in a surgical removal of one key. This article traces the actual code path, separates default-install behavior from persistent drop-in behavior, and gives you instrumentation to confirm which failure shape you have before you change anything.

Symptom: a flush, then a burst of options queries

The observable pattern is consistent enough to describe without inventing an incident. A request runs wp_cache_flush() — often from a plugin’s settings save, a deploy hook, a cron job, or a poorly scoped init callback. Later in the same request, or in concurrent requests arriving during the flush window, code calls get_option() or alloptions(). The alloptions key is absent from the object cache, so wp_load_alloptions() runs its database query and repopulates the key.

In a default install, that repopulation is bounded to the request. The object cache documentation is explicit: by default the cache is non-persistent, and data stored in it resides in memory only for the duration of the request (WP_Object_Cache). A single request that flushes and then reloads alloptions pays one extra query. That is not a stampede. The interesting failure mode requires a persistent object cache drop-in, where the flush affects shared state across concurrent requests.

Trace: what wp_cache_flush() actually calls

The wrapper is thin. The developer reference for wp_cache_flush() shows the entire function:

function wp_cache_flush() {
    global $wp_object_cache;
    return $wp_object_cache->flush();
}

It delegates to the global object cache’s flush() method and returns its boolean result. In core, that method is equally blunt. The WP_Object_Cache::flush() reference shows:

public function flush() {
    $this->cache = array();
    return true;
}

Core does not delete the alloptions key. It resets the entire in-memory cache array to empty and returns true. Every key in every group is gone from the runtime cache, including alloptions, but not because of a targeted delete. The distinction is not pedantic: if you are looking for a code path that removes one key, you will not find it in core, and you will misdiagnose the problem.

The place where the story changes is the drop-in. The object cache documentation notes that the object cache can be replaced by other caching mechanisms via a file in wp-content, and that if that file exists, the core class is not included (WP_Object_Cache). A persistent drop-in supplies its own flush(). Whether that flush clears the entire backend, clears a namespace, or performs a targeted delete is a property of the drop-in, not of core. No drop-in source was retrieved for this article, so the correct statement is conditional: if your drop-in’s flush() deletes alloptions specifically, that behavior lives in the drop-in and must be read there. Core’s own flush() does not do it.

Trace: how wp_load_alloptions() repopulates the key

The repopulation path is where the cost accumulates. The wp_load_alloptions() reference shows the sequence. First, a filter can short-circuit the function entirely:

$alloptions = apply_filters( 'pre_wp_load_alloptions', null, $force_cache );
if ( is_array( $alloptions ) ) {
    return $alloptions;
}

If no filter returns an array, the function attempts a cache read:

$alloptions = wp_cache_get( 'alloptions', 'options', $force_cache );

On a miss, it queries the options table for autoloaded values, with a fallback to all options if that query returns nothing:

$alloptions_db = $wpdb->get_results( "SELECT option_name, option_value FROM $wpdb->options WHERE autoload IN ( '" . implode( "', '", esc_sql( wp_autoload_values_to_autoload() ) ) . "' )" );
if ( ! $alloptions_db ) {
    $alloptions_db = $wpdb->get_results( "SELECT option_name, option_value FROM $wpdb->options" );
}

After building the array, it applies pre_cache_alloptions and stores the result back into the cache:

wp_cache_add( 'alloptions', $alloptions, 'options' );

Finally, it applies the alloptions filter and returns the array (alloptions hook). The important detail for stampede analysis is the use of wp_cache_add(), not wp_cache_set(). add() only writes if the key does not already exist. Under concurrency, multiple requests can each miss, each run the query, and each attempt an add; only one add wins, but every request has already paid for the query. That is the shape of the stampede: repeated read work and repeated write attempts, not a single deletion event.

Root cause: concurrent misses during repopulation

Model the failure as a cache miss storm on a shared key. A persistent drop-in holds alloptions in shared storage. A flush removes it. Requests arriving before the first repopulation completes all miss. Each runs the options query. Each attempts wp_cache_add(). The database sees a burst of identical reads, and the cache backend sees a burst of competing adds.

This pattern is documented in an analogous case. A Trac mailing-list post about flush_rewrite_rules() describes what happens when rewrite_rules, which is stored in alloptions, is invalidated frequently: “you have that cache key being invalidated and set quite frequently because all the requests are touching it. That introduces both some fun alloptions race conditions in general, but also makes it possible that the object cache (Memcached specifically) can enter into a stampede condition” (wp-trac, August 2023). The same post describes the first stampede: “Each request during the time period will find rewrite_rules empty and attempt to generate and save the rules.”

That is evidence of the pattern, not of your specific incident. The post concerns flush_rewrite_rules(), not wp_cache_flush(), and it does not claim that core’s flush deletes alloptions. Use it to reason about the mechanism, then instrument your own install to confirm whether the mechanism is active.

One more core behavior belongs in the model. The object cache documentation warns that calling wp_cache_flush_group() blindly may cause the entire cache to be flushed if the backend does not support group flushing, and recommends checking wp_cache_supports( 'flush_group' ) first (WP_Object_Cache). A plugin that intends to clear one group and instead clears everything produces the same alloptions miss storm as an explicit full flush. That is a plausible root cause worth checking before you blame wp_cache_flush() itself.

Fix: instrument before you theorize

The fix depends on which failure shape you actually have. Do not start by replacing wp_cache_flush() with a targeted delete. Start by confirming what your drop-in’s flush() does and whether the alloptions key is being missed repeatedly.

1. Read the drop-in’s flush()

Open the object cache drop-in in wp-content and read its flush() method. Determine whether it clears the entire backend, clears a namespace, or deletes specific keys. If it deletes alloptions specifically, that is the code path to examine. If it clears everything, the alloptions miss is a side effect of a full flush, and the question becomes why a full flush is running during normal traffic.

2. Log flush calls with a backtrace

A flush that runs on every request or inside a loop is a different problem from a one-time deploy flush. Add temporary logging around the call site, or use a query-monitor-style backtrace capture, to record who calls wp_cache_flush() and how often. The wrapper itself is trivial; the caller is the interesting part.

3. Log alloptions misses

Hook the cache read path for the options group and record misses on the alloptions key. A temporary mu-plugin can wrap the drop-in’s get() or use the pre_wp_load_alloptions filter to observe calls without altering behavior. The goal is a count per request and a timestamp, so you can correlate misses with flush events.

4. Check the filters

pre_wp_load_alloptions can short-circuit the function by returning an array, and pre_cache_alloptions can alter the array before it is cached. A plugin that returns an incomplete array from pre_wp_load_alloptions can cause repeated reloads or inconsistent option values. Grep for both filter names in your plugin set before assuming the cache backend is at fault.

5. Check for blind group flushes

Search for wp_cache_flush_group() calls that are not guarded by wp_cache_supports( 'flush_group' ). On a backend without group-flush support, those calls degrade to a full flush and produce the same miss storm.

Verification: what a confirmed stampede looks like

After instrumentation, a confirmed alloptions stampede has a specific signature. The options query appears multiple times in a short window across concurrent requests, not once per request. The cache miss counter for the options group rises in step with flush events. The wp_cache_add() calls for alloptions compete, and the backend may show eviction or write contention if it is Memcached-like. If you see one extra query per request after a single flush, you have repopulation cost, not a stampede.

For a default install, the verification is simpler: confirm that the object cache is non-persistent and that the miss is bounded to the request. The documentation states that default cache data does not persist across page loads (WP_Object_Cache). If your install has no persistent drop-in, the cross-request stampede cannot occur, and the investigation should focus on why the flush is running at all.

Decision rule

Treat wp_cache_flush() as a blunt instrument whose real cost is repopulation work, not a single key deletion. Core’s flush() resets the entire in-memory cache array and returns true; it does not surgically remove alloptions. If your drop-in performs a targeted delete, that is the drop-in’s behavior and should be read and tested there. Prefer a targeted wp_cache_delete( 'alloptions', 'options' ) only after you have verified the drop-in’s semantics for that key and group and confirmed that the full flush is the actual source of the miss storm. Until then, instrument the flush call sites, the alloptions misses, and the filters, and let the trace tell you which failure shape you have.

FAQ

Does wp_cache_flush() delete the alloptions key?

Not in core. Core’s WP_Object_Cache::flush() sets the entire in-memory cache array to empty and returns true. The alloptions key disappears as part of that reset, but there is no targeted deletion. A persistent drop-in can implement flush() differently, so check the drop-in source.

Why does alloptions reload after a flush?

wp_load_alloptions() reads alloptions from the options cache group. After a flush, that read misses, so the function queries the options table for autoloaded values and repopulates the key with wp_cache_add(). That is expected behavior, and the cost is the query plus the add attempt.

Can a stampede happen without a persistent object cache?

Not across requests. The default object cache is non-persistent and lives only for the duration of the request. A flush and reload within one request costs one extra query. The cross-request stampede requires shared cache state, which means a persistent drop-in.

What should I check first?

Read the drop-in’s flush() method, then log who calls wp_cache_flush() and how often. Check for unguarded wp_cache_flush_group() calls and for pre_wp_load_alloptions filters that return arrays. Confirm the miss pattern before changing the flush strategy.