WordPress transients are the key-value cache layer that stores expensive query results, remote API responses, and computed fragments in the options table or an external object cache. In a multisite network, the same transient key can resolve to different values on different blogs, or worse, the same value can leak across blogs because the key is not namespaced per site. This article documents the exact collision mechanics, shows reproducible SQL and WP-CLI evidence, and gives a namespacing pattern that works in both single-site and multisite installs.
If you maintain a production multisite install for a small-to-mid publishing team, you have probably seen a transient from blog 2 appear in blog 3 after a cache flush, or a scheduled event fire with the wrong site context. The root cause is rarely the object cache backend. It is the key construction. WordPress core does not automatically prefix transient keys with the current blog ID in all contexts, and plugins that call set_transient() or get_transient() without a site-aware prefix inherit that behavior.
How WordPress Stores Transients in the Database
When no persistent object cache is active, WordPress stores transients in the wp_options table. The option name is built by prepending _transient_ or _transient_timeout_ to the key you pass. For example, set_transient( 'weather_london', $data, 600 ) writes two rows:
_transient_weather_london— the serialized value_transient_timeout_weather_london— the Unix timestamp when the transient expires
In a multisite network, each blog has its own wp_2_options, wp_3_options, and so on. That physical separation prevents most cross-blog collisions at the database level. The problem appears when a plugin or theme uses a global cache group, a shared object cache, or a network-wide transient function without a blog-specific key.
Reproducing the Collision with SQL
Run this query on a multisite install with at least two blogs:
SELECT option_name, option_value
FROM wp_2_options
WHERE option_name LIKE '%weather_london%';
SELECT option_name, option_value
FROM wp_3_options
WHERE option_name LIKE '%weather_london%';
If a plugin called set_transient( 'weather_london', $data, 600 ) while switched to blog 2, the first query returns the value. If the same plugin later called get_transient( 'weather_london' ) while switched to blog 3, the second query returns nothing — unless the plugin used switch_to_blog() incorrectly or stored the transient in a global group. That is the first failure mode: a missing value that looks like a cache miss but is actually a key scoping error.
Reproducing the Collision with WP-CLI
Use wp transient commands to see the same behavior from the command line:
wp transient set weather_london 'rain' 600 --url=blog2.example.com
wp transient get weather_london --url=blog2.example.com
wp transient get weather_london --url=blog3.example.com
The first get returns rain. The second returns an empty result because blog 3 has no such transient. That is expected. The collision happens when a plugin stores the transient in a network-wide cache group or uses set_site_transient() with a key that is not unique per blog.
The Exact Collision: Network-Wide Transients and Shared Keys
WordPress has two transient APIs that operate at the network level:
set_site_transient( $key, $value, $expiration )get_site_transient( $key )
These functions store data in the wp_sitemeta table or in a network-wide cache group. The key is not prefixed with a blog ID. If two plugins on different blogs both use set_site_transient( 'weather_london', ... ), the second call overwrites the first. The value from blog 2 leaks into blog 3, and the expiration timestamp is shared. This is the collision that causes real production bugs: a weather widget on blog 3 suddenly shows London weather because blog 2 updated the same key.
To reproduce this, run the following on a multisite install:
wp eval 'set_site_transient( "weather_london", "rain", 600 );' --url=blog2.example.com
wp eval 'set_site_transient( "weather_london", "sunny", 600 );' --url=blog3.example.com
wp eval 'var_dump( get_site_transient( "weather_london" ) );' --url=blog2.example.com
The output is sunny, not rain. Blog 2’s value was overwritten by blog 3 because both used the same network-wide key. This is not a bug in WordPress core; it is a consequence of the API contract. set_site_transient() is designed for network-wide data like update checks, not per-blog data.
Why Plugins Accidentally Use Network-Wide Transients
Most collisions come from three patterns:
- Copy-paste from single-site examples. A developer reads the Codex example for
set_transient()and uses it in a multisite plugin without checking the context. The plugin works on a single site, but on multisite the transient is stored in the current blog’s options table only if the plugin is running in that blog’s context. If the plugin runs in a network admin context or during a cron job that iterates over blogs, the transient may be stored in the wrong blog’s table. - Using
set_site_transient()for per-blog data. Some developers assumesitemeans the current site, not the network. They useset_site_transient()for per-blog data and create the exact collision described above. - Hardcoded keys in shared libraries. A theme or plugin that is network-activated may use a hardcoded key like
my_plugin_latest_postsin a global cache group. On a single site, that key is fine. On multisite, every blog shares the same key in the global group, so the value from the first blog to write wins.
How to Namespace Transient Keys Correctly
The fix is to make the transient key unique per blog and per context. The simplest pattern is to include the current blog ID in the key:
$blog_id = get_current_blog_id();
$key = 'weather_london_' . $blog_id;
set_transient( $key, $data, 600 );
This works for per-blog transients stored in the blog’s own options table. The key is unique across blogs because the blog ID is part of the key. When you retrieve the transient, you must build the same key:
$blog_id = get_current_blog_id();
$key = 'weather_london_' . $blog_id;
$data = get_transient( $key );
If you are using a persistent object cache like Redis or Memcached, the same pattern applies. The object cache backend may use a global key space, so the blog ID in the key prevents collisions there too.
Namespacing for Network-Wide Transients
If you genuinely need a network-wide transient, use set_site_transient() but make the key unique to the data you are storing. For example, if you are caching a network-wide list of active plugins, use a key like active_plugins_network. If you are caching per-blog data in a network-wide transient, include the blog ID in the key:
$blog_id = get_current_blog_id();
$key = 'weather_london_' . $blog_id;
set_site_transient( $key, $data, 600 );
This prevents the collision because blog 2 and blog 3 now use different keys. The data is still stored in the network-wide cache, but each blog’s value is isolated.
Using a Prefix Constant
For plugins that are distributed or used across many sites, define a prefix constant and use it in every transient call:
define( 'MY_PLUGIN_PREFIX', 'my_plugin_' );
function my_plugin_get_cached_weather( $city ) {
$blog_id = get_current_blog_id();
$key = MY_PLUGIN_PREFIX . 'weather_' . $city . '_' . $blog_id;
return get_transient( $key );
}
This makes the key self-documenting and reduces the chance of a typo. It also makes it easy to flush all transients for the plugin by deleting keys that start with the prefix.
Flushing Transients Without Causing Collisions
When you flush transients, you must be careful not to delete transients that belong to other blogs. The delete_transient() function only deletes the transient for the current blog if you use a per-blog key. If you use a network-wide key, delete_site_transient() deletes it for the entire network.
To flush all transients for a specific blog, use WP-CLI:
wp transient delete --all --url=blog2.example.com
This deletes only the transients stored in blog 2’s options table. It does not touch blog 3’s transients. If you have used network-wide transients with blog-specific keys, you must delete them individually or use a custom cleanup routine.
Real Failure Mode: Cron Jobs and Switch_to_blog
A common production failure happens when a cron job iterates over blogs and calls switch_to_blog(). The transient key is built before the switch, so it uses the wrong blog ID. For example:
$blogs = get_sites();
foreach ( $blogs as $blog ) {
switch_to_blog( $blog->blog_id );
$key = 'weather_london_' . get_current_blog_id();
set_transient( $key, $data, 600 );
restore_current_blog();
}
This works because the key is built after the switch. But if the key is built before the switch, every blog gets the same key, and the transient is stored in the wrong blog’s options table. The fix is to always build the key inside the switched context.
Testing Your Transient Keys
To verify that your transient keys are namespaced correctly, run this WP-CLI command on a multisite install:
wp eval 'var_dump( get_current_blog_id() );' --url=blog2.example.com
wp eval 'var_dump( get_current_blog_id() );' --url=blog3.example.com
Then set a transient on blog 2 and try to get it on blog 3:
wp transient set weather_london_2 'rain' 600 --url=blog2.example.com
wp transient get weather_london_2 --url=blog3.example.com
The second command should return an empty result. If it returns rain, your object cache backend is sharing keys across blogs, and you need to add a blog ID to the key or configure the cache backend to use per-blog key prefixes.
FAQ
Why do transients collide in multisite but not in single-site installs?
In a single-site install, there is only one options table and one blog ID. The transient key is unique by default. In multisite, each blog has its own options table, but network-wide transients and shared object cache groups use a global key space. If a plugin uses the same key for per-blog data without including the blog ID, the values collide.
How can I tell if a transient collision is happening on my site?
Look for symptoms like a widget showing the wrong content on one blog, a scheduled event firing with the wrong site context, or a transient value that changes unexpectedly after another blog updates. You can also query the options tables directly to see if the same transient key exists in multiple blogs with different values.
What is the difference between set_transient() and set_site_transient()?
set_transient() stores data in the current blog’s options table or in a per-blog cache group. set_site_transient() stores data in the network-wide wp_sitemeta table or in a global cache group. Use set_transient() for per-blog data and set_site_transient() only for data that is truly network-wide.
Does WordPress core automatically namespace transient keys per blog?
No. WordPress core does not automatically prefix transient keys with the blog ID. The set_transient() function stores the key exactly as you pass it, in the current blog’s options table. The physical table separation prevents most collisions, but network-wide transients and shared object cache groups require manual namespacing.
For more on WordPress database behavior and troubleshooting, see What to Fix First When a New WordPress Site Says Nothing Found.


