{"id":19020801,"date":"2026-09-15T15:20:39","date_gmt":"2026-09-15T15:20:39","guid":{"rendered":"https:\/\/wordpress.org\/support\/topic\/wp_cache_replace-creates-a-missing-redis-key-while-returning-false\/"},"modified":"2026-09-15T15:27:52","modified_gmt":"2026-09-15T15:27:52","slug":"wp_cache_replace-creates-a-missing-redis-key-while-returning-false","status":"publish","type":"topic","link":"https:\/\/wordpress.org\/support\/topic\/wp_cache_replace-creates-a-missing-redis-key-while-returning-false\/","title":{"rendered":"wp_cache_replace() creates a missing Redis key while returning false"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Hi,<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Thanks for maintaining Redis Object Cache.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">While testing version 2.8.0 against a Redis\/PhpRedis instance, I found behavior that appears to violate the WordPress <code>wp_cache_replace()<\/code> contract.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">WordPress documents <code>wp_cache_replace()<\/code> as replacing a value only when the key already exists:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">https:\/\/developer.wordpress.org\/reference\/functions\/wp_cache_replace\/<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Environment<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Here&#8217;s my config:<\/p>\n\n\n\n<ul>\n<li>Redis Object Cache: 2.8.0<\/li>\n\n\n\n<li>Drop-in: unmodified <code>includes\/object-cache.php<\/code> from the official 2.8.0 release<\/li>\n\n\n\n<li>Drop-in SHA-256: <code>713e9a12018e865f7daf71d84e6257b7756251560f04aac6cc6b5b15bf9d0b33<\/code><\/li>\n\n\n\n<li>Client: PhpRedis 6.2.0<\/li>\n\n\n\n<li>Redis server: 8.0.2<\/li>\n\n\n\n<li>PHP: 8.4.24<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Minimal repro. (With Redis Object Cache enabled and connected, run):<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>wp eval '\n$key = \"redis-cache-replace-repro-\" . wp_generate_uuid4();\n$group = \"redis-cache-replace-repro\";\n\n$replace_result = wp_cache_replace($key, \"created\", $group);\n\n$found = null;\n$value = wp_cache_get($key, $group, true, $found);\n\nvar_export(&#091;\n    \"replace_result\" =&gt; $replace_result,\n    \"found_after_replace\" =&gt; $found,\n    \"value_after_replace\" =&gt; $value,\n]);\n\nwp_cache_delete($key, $group);\n'<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Expected result<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Because the key did not exist, <code>wp_cache_replace()<\/code> should return <code>false<\/code> and leave it absent:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>array (\n  'replace_result' =&gt; false,\n  'found_after_replace' =&gt; false,\n  'value_after_replace' =&gt; false,\n)<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Actual result<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><code>wp_cache_replace()<\/code> returns <code>false<\/code>, but the key has been created in Redis:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>array (\n  'replace_result' =&gt; false,\n  'found_after_replace' =&gt; true,\n  'value_after_replace' =&gt; 'created',\n)<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">I also reproduced this directly, outside of WordPress. The observed values were:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>bool(false)\nbool(true)\nstring(7) \"created\"<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>Suspected cause<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">In <code>WP_Object_Cache::add_or_replace()<\/code>, <code>add()<\/code> uses an atomic conditional Redis <code>SET ... NX<\/code>, but the <code>replace()<\/code> branch performs an unconditional <code>SET<\/code> or <code>SETEX<\/code>:<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">https:\/\/github.com\/rhubarbgroup\/redis-cache\/blob\/2.8.0\/includes\/object-cache.php#L1307-L1375<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The method checks the request-local <code>$this-&gt;cache<\/code> only after the Redis write has already occurred. Consequently:<\/p>\n\n\n\n<ol>\n<li>Replacing a missing key creates it while returning <code>false<\/code>.<\/li>\n\n\n\n<li>Replacing a key that exists in Redis but has not been loaded into the current request changes it while returning <code>false<\/code>.<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">Would using Redis <code>SET ... XX<\/code> for the persistent <code>replace()<\/code> path be the appropriate fix? That would make the existence check and write atomic, analogous to the existing <code>NX<\/code> implementation for <code>add()<\/code>.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">I noticed the repository already contains <code>testReplace()<\/code> and <code>testWpCacheReplace()<\/code>. Is it possible those tests are exercising WordPress core\u2019s runtime <code>WP_Object_Cache<\/code> rather than the Redis drop-in because WordPress is bootstrapped before the drop-in is copied? I may be overlooking part of the test setup, so confirmation would be appreciated.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Cheers.<br \/>Dan<\/p>\n","protected":false},"template":"","class_list":["post-19020801","topic","type-topic","status-publish","hentry"],"jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/wordpress.org\/support\/wp-json\/wp\/v2\/topic\/19020801","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/wordpress.org\/support\/wp-json\/wp\/v2\/topic"}],"about":[{"href":"https:\/\/wordpress.org\/support\/wp-json\/wp\/v2\/types\/topic"}],"version-history":[{"count":2,"href":"https:\/\/wordpress.org\/support\/wp-json\/wp\/v2\/topic\/19020801\/revisions"}],"predecessor-version":[{"id":19020803,"href":"https:\/\/wordpress.org\/support\/wp-json\/wp\/v2\/topic\/19020801\/revisions\/19020803"}],"wp:attachment":[{"href":"https:\/\/wordpress.org\/support\/wp-json\/wp\/v2\/media?parent=19020801"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}