{"id":18984903,"date":"2026-08-05T15:40:08","date_gmt":"2026-08-05T15:40:08","guid":{"rendered":"https:\/\/wordpress.org\/support\/topic\/request-teardown-blocks-7-60s-redis-connection-never-closed-explicitly\/"},"modified":"2026-08-05T15:40:08","modified_gmt":"2026-08-05T15:40:08","slug":"request-teardown-blocks-7-60s-redis-connection-never-closed-explicitly","status":"publish","type":"topic","link":"https:\/\/wordpress.org\/support\/topic\/request-teardown-blocks-7-60s-redis-connection-never-closed-explicitly\/","title":{"rendered":"Request teardown blocks 7-60s: Redis connection never closed explicitly"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\"><strong>Environment.<\/strong> Redis Object Cache 2.8.0 (drop-in reports Version 2.8.0, md5 e27667c2e4d2aee614f3253e285f3e45) \u00b7 PhpRedis 6.3.0 (Predis\/Relay\/Credis not loaded) \u00b7 igbinary 3.2.16 \u00b7 PHP 8.3.33 on PHP-FPM (pm = dynamic, max_children = 20, unix socket) \u00b7 Redis 7.4.10 in a <em>separate container, reached over TCP by service name, AUTH required<\/em> \u00b7 WP_REDIS_TIMEOUT = 1, WP_REDIS_READ_TIMEOUT = 1 \u00b7 nginx with the default 60s fastcgi_read_timeout \u00b7 single site \u00b7 78 active plugins (WooCommerce + Subscriptions) \u00b7 Docker\/EasyPanel on a KVM VPS.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Throughout everything below, <code>wp redis status<\/code> reports Status: Connected \/ Drop-in: Valid \/ Ping: 1 \/ Errors: []. Redis itself is healthy: a pure PhpRedis GET\/SET round-trip from the same container measures 4.5 ms.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>SYMPTOM<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Every FastCGI request that boots WordPress with the drop-in active spends <strong>7 to 60 seconds after all PHP userland code has finished<\/strong>, in PHP\u2019s automatic shutdown phase. The response is fully generated in under one second and then sits there, undelivered, while the process tears itself down.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Releasing the cache object explicitly \u2014 a single <code>unset($GLOBALS['wp_object_cache'])<\/code> at the very end of the request, while the request is still alive \u2014 removes the delay entirely and costs about 12 ms. The exact same work, done microseconds later by PHP\u2019s own teardown, costs 7 to 60 seconds.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The drop-in has <strong>no __destruct(), no close() method and no register_shutdown_function()<\/strong> (grep against the installed 2.8.0 file: __destruct = 0, function close = 0, register_shutdown_function = 0). The connection is left for PHP to reap implicitly, and that is where the time goes.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>EVIDENCE 1 \u2014 the time is spent after PHP is done<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Comparing REQUEST_TIME_FLOAT measured inside the request against the client wall clock on the same host (curl over 127.0.0.1, no proxy in between):<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>request   PHP finished at   client got response at   gap\n  #3          +485 ms              +31,270 ms         30.8 s\n  #5          +784 ms              +46,470 ms         45.7 s\n  #8          +616 ms              +46,315 ms         45.7 s<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">CPU stays at 0-1% during the gap. Sampling the worker state 419 times during the waits gave 409 samples sleeping and 10 running \u2014 it is blocking, not computing. Some requests stall at exactly 60.001157 s, which is nginx giving up at its default fastcgi_read_timeout, so the teardown can block indefinitely rather than for a bounded interval.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>EVIDENCE 2 \u2014 bisection isolates it to one object<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Five identical PHP files in the web root, each doing require of wp-load.php and then differing by a <strong>single line<\/strong> at the end. Four runs each, through FastCGI:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>x0  (nothing, control)                      7.098 \/ 60.014 \/ 60.001 \/ 16.466 s\nx1  unset($GLOBALS&#091;'wp_object_cache']);      0.585 \/  0.543 \/  0.542 \/  0.606 s\nx2  unset($GLOBALS&#091;'wpdb']);                11.392 \/ 11.585 \/  1.549 \/ 31.347 s\nx3  gc_collect_cycles();                    60.004 \/ 11.785 \/  7.285 \/ 31.510 s\nx4  both unsets                              0.768 \/  0.786 \/  0.783 \/  0.783 s<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Only the variants that release the object cache are fast. Forcing a GC cycle changes nothing, so this is not cycle collection, and wpdb alone does not explain it either.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>EVIDENCE 3 \u2014 what was ruled out, with numbers<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>static file via nginx ................. 0.7 - 1.4 ms      fine\nbare PHP via FastCGI, no WordPress .... 0.9 - 4.5 ms      fine\nsession_start() ....................... 1.5 - 5.4 ms      fine\nDNS (including NXDOMAIN) .............. 4 - 10 ms         fine\nOPcache hit rate ...................... 99.56%            fine\ndisk: stat of 24,751 PHP files ........ 0.58 s            fine\nRedis round-trip, pure PhpRedis ....... 4.5 ms            fine\nMySQL round-trip, pure ................ 6 - 7 ms          fine\nWordPress + 78 plugins via WP-CLI ..... 3.5 - 8.5 s, never stalls\nWordPress + 78 plugins via FastCGI .... 1.6 s - 60.0 s    the stall<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Worker exhaustion was ruled out too: 10 parallel requests all finished internally in 617-1004 ms, the FPM listen queue was 0 and active processes 0 at the moment of the stalls, and pm = static with 20 pre-forked workers changed nothing. WooCommerce webhooks: 0 deliveries during 8 stalled requests. Output buffering: default handler only, 0 ms.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">The WP-CLI versus FastCGI contrast is the important one \u2014 same WordPress, same plugins, same Redis, same database. The stall appears only in the SAPI where delivering the response depends on the process reaching the end of its teardown.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>WORKAROUND NOW RUNNING IN PRODUCTION<\/strong><\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>add_action( 'shutdown', function () {\n    register_shutdown_function( function () {\n        if ( isset( $GLOBALS&#091;'wp_object_cache'] ) ) {\n            unset( $GLOBALS&#091;'wp_object_cache'] );\n        }\n    } );\n}, PHP_INT_MAX );<\/code><\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Before and after on wp-login.php, five runs each: <strong>46.504 s<\/strong> before, <strong>0.591 \/ 0.588 \/ 0.575 \/ 0.836 \/ 0.913 s<\/strong> after. wp-admin 0.875 s, admin-ajax.php 0.61-1.93 s, front-end 0.115 s. No functional regressions; several hours later wp redis status still reports Connected \/ Valid \/ Ping 1 \/ Errors [].<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>SUGGESTED FIX<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Close the connection deterministically while the request is still alive, instead of leaving it to PHP\u2019s implicit teardown. Either a close() method on WP_Object_Cache calling redis-&gt;close(), invoked from a late shutdown hook, or an explicit release of the object at the end of the request. A __destruct() on its own would still run during the same implicit teardown and probably would not help. An opt-out constant would be a reasonable escape hatch for persistent-connection setups.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\"><strong>WHAT I COULD NOT DETERMINE<\/strong><\/p>\n\n\n\n<p class=\"wp-block-paragraph\">I could not get inside the blocking call: the container has no CAP_SYS_PTRACE, so strace and \/proc\/&lt;pid&gt;\/syscall were unavailable. My working hypothesis is that when the PhpRedis Redis object is released during PHP\u2019s request shutdown, the socket teardown against an authenticated, TCP, cross-container Redis blocks, and WP_REDIS_TIMEOUT \/ WP_REDIS_READ_TIMEOUT do not apply on that path. The observed clustering around 6.4 s and at multiples of 10 s is consistent with retransmission backoff, but I cannot prove it.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Happy to run further instrumentation on this host if that would help narrow it down. Thanks for the plugin.<\/p>\n","protected":false},"template":"","class_list":["post-18984903","topic","type-topic","status-publish","hentry"],"jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/wordpress.org\/support\/wp-json\/wp\/v2\/topic\/18984903","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":0,"href":"https:\/\/wordpress.org\/support\/wp-json\/wp\/v2\/topic\/18984903\/revisions"}],"wp:attachment":[{"href":"https:\/\/wordpress.org\/support\/wp-json\/wp\/v2\/media?parent=18984903"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}