You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Keep-alive connection reuse stalls up to ~500ms on an idle event loop (possible regression in 8.8.0) #5600
Since undici 8.8.0, reusing an idle keep-alive socket can stall for up to ~500ms before the
request is written to the wire. The request is created, queued, and then simply sits there.
This was discovered using a pool with a single connection.
I think cause is scheduleIdleSocketValidation() in lib/dispatcher/client-h1.js, which was changed
from an unref'd setTimeout(…, 0) to an unref'd setImmediate().
Used / Tested Environment
undici
8.9.0 (also reproduces on 8.8.0)
last good
8.7.0
Node.js
v24.18.0
Platform
macOS (darwin 25.5.0, arm64)
Reproduction
Standalone reproduction script:
importassertfrom'node:assert'importhttpfrom'node:http'import{test}from'node:test'import{Pool}from'undici'constREQUESTS=6constSERVER_DELAY_MS=10constMAX_TOTAL_MS=400test('reusing an idle keep-alive socket must not stall',async(t)=>{constserver=http.createServer((req,res)=>{setTimeout(()=>{res.writeHead(200,{'content-length': 2})res.end('ok')},SERVER_DELAY_MS)})awaitnewPromise((resolve)=>server.listen(0,resolve))constpool=newPool(`http://127.0.0.1:${server.address().port}`,{connections: 1})t.after(async()=>{awaitpool.close()server.close()})constdurations=[]conststart=Date.now()for(leti=0;i<REQUESTS;i++){constrequestStart=Date.now()constres=awaitpool.request({path: '/',method: 'GET'})awaitres.body.text()durations.push(Date.now()-requestStart)}consttotal=Date.now()-startt.diagnostic(`per-request: ${durations.map((d)=>`${d}ms`).join(', ')}`)t.diagnostic(`total: ${total}ms`)assert.ok(total<MAX_TOTAL_MS,`${REQUESTS} sequential requests took ${total}ms (per-request: ${durations.join('ms, ')}ms). `+`The server replies in ${SERVER_DELAY_MS}ms, so this should be well under ${MAX_TOTAL_MS}ms.`)})
Expected Behavior
reusing an idle keep-alive socket must not stall (84.42725ms)
per-request: 20ms, 13ms, 13ms, 12ms, 13ms, 13ms
total: 84ms
The server takes 10ms to respond, so every request should be ~10–20ms - and on 8.7.0 every one is.
On 8.9.0, reuses of the keep-alive socket cost hundreds of milliseconds each.
Actual Behavior
reusing an idle keep-alive socket must not stall (1482.400041ms)
per-request: 20ms, 458ms, 34ms, 51ms, 450ms, 464ms
total: 1477ms
AssertionError [ERR_ASSERTION]: 6 sequential requests took 1477ms
(per-request: 20ms, 458ms, 34ms, 51ms, 450ms, 464ms).
The server replies in 10ms, so this should be well under 400ms.
Activity
fix: revert idle socket validation to setTimeout(0) to prevent stall …
fix: work around undici 8.8+ idle socket keepalive deadlock
docs(test): reference the upstream nodejs/undici issue for the fetch(…
chore(deps): update undici to v8.10.0
fix(test): enforce e2e poll budgets and stop reusing idle connections (…
feat(sdk)!: add options.headers, fetchOptions, and fetch (#456)
fix(test): stop using fetch() in ttlResetOnWrite / static-cache-heade…