Skip to content

fix: revert idle socket validation to setTimeout(0) to prevent stall on idle event loop - #5606

Open
marceli1404 wants to merge 3 commits into
nodejs:mainfrom
marceli1404:fix/idle-socket-validation-set-timeout
Open

fix: revert idle socket validation to setTimeout(0) to prevent stall on idle event loop#5606
marceli1404 wants to merge 3 commits into
nodejs:mainfrom
marceli1404:fix/idle-socket-validation-set-timeout

Conversation

@marceli1404

Copy link
Copy Markdown

Summary

Reverts scheduleIdleSocketValidation in client-h1.js from setImmediate() back to setTimeout(0), along with the corresponding clearImmediateclearTimeout.

Background

PR #5499 changed setTimeout(0)setImmediate() to remove the ~1ms timer floor on idle socket validation. While that change performed well under sustained load (where the event loop is continuously busy), it causes up to ~500ms stalls when reusing an idle keep-alive socket on an otherwise idle event loop.

Root Cause

When a keep-alive socket is reused and the event loop is otherwise idle:

  1. The previous request completes → resumeH1() unrefs the socket (socket.unref())
  2. The next request is queued → scheduleIdleSocketValidation sets an unref()'d setImmediate
  3. The socket is re-ref'd but the setImmediate fires in the check phase (after poll)
  4. Since the socket was just ref'd but has no pending I/O, the poll phase waits for its default timeout before proceeding to the check phase — this can be up to 500ms on some platforms

setTimeout(0) fires in the timers phase (before poll), so the callback runs promptly regardless of poll timeout, at the cost of ~1ms timer resolution.

Reproduction

Script in the issue shows sequential requests on a 1-connection pool taking 20ms, 458ms, 34ms, 51ms, 450ms, 464ms instead of ~13ms each.

Fix

This PR reverts the approach from #5499. The original motivation (removing the timer floor) is worthwhile, but it introduces an order-of-magnitude worse regression on idle event loops. A hybrid approach (check-phase scheduling with poll-timeout prevention) could be explored separately.

Fixes #5600

…on idle event loop

The change from setTimeout(0) to setImmediate() in nodejs#5499 introduced a
regression where reusing an idle keep-alive connection could stall for
~500ms. The root cause is the interaction between setImmediate().unref()
and socket.unref() in the idle state: when the socket is unref'd and
the only remaining handle is an unref'd setImmediate, the event loop
poll phase waits for its timeout before reaching the check phase.

setTimeout(0) fires in the timers phase (before poll), avoiding this
problem entirely at the cost of ~1ms timer resolution per reuse.

Fixes nodejs#5600

@mcollina mcollina left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a test?

@codecov-commenter

codecov-commenter commented Jul 28, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.49%. Comparing base (8453a77) to head (b1ac19a).
⚠️ Report is 2 commits behind head on main.

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #5606   +/-   ##
=======================================
  Coverage   93.49%   93.49%           
=======================================
  Files         110      110           
  Lines       38281    38303   +22     
=======================================
+ Hits        35789    35812   +23     
+ Misses       2492     2491    -1     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@marceli1404

Copy link
Copy Markdown
Author

Added a regression test for the idle socket reuse stall ( est/node-test/keep-alive-reuse.js). It makes 6 sequential requests on a single-connection pool with 50ms server delay and asserts each request completes within 200ms. The test passes on this branch.

Replace wall-clock timing and artificial server delays with server
connection/request event coordination. The test timeout catches the
setImmediate poll-phase stall regression without flaky Date.now checks.

Signed-off-by: Matteo Collina <hello@matteocollina.com>

@mcollina mcollina left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. The setTimeout(0) revert is the right fix for the idle keep-alive stall, and the regression test now drives progress via server connection/request events instead of wall-clock thresholds.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Keep-alive connection reuse stalls up to ~500ms on an idle event loop (possible regression in 8.8.0)

3 participants