| name | node-http-proxy-ttfb-timeout |
| description | Fix HTTP proxy requests hanging forever on keep-alive connections in Node.js.
Use when: (1) First request works but second request hangs indefinitely,
(2) socket.setTimeout or manual timer on 'socket' event doesn't fire for
reused connections, (3) Proxy sits in front of a tunnel/origin that may go
down (e.g., laptop sleeping), (4) Need reliable TTFB (time-to-first-byte)
timeout that works for both fresh and keep-alive connections. Applies to
http.request, https.request, and reverse proxy implementations.
|
| author | Claude Code |
| version | 1.0.0 |
| date | "2026-02-07T00:00:00.000Z" |
Node.js HTTP Proxy TTFB Timeout (Keep-Alive Safe)
Problem
When implementing HTTP proxy/reverse-proxy code with TTFB (time-to-first-byte) timeouts,
socket-based timeout logic fails on keep-alive connections. The first request works fine,
but subsequent requests on reused sockets hang forever even though the upstream is dead.
Context / Trigger Conditions
- First HTTP request through proxy succeeds
- Second request (on same keep-alive connection) hangs indefinitely
- Upstream is a tunnel (cloudflared, ngrok) or service that can go offline
- Using
socket.setTimeout() or proxyReq.on('socket', ...) for timeout logic
- Timeout callback never fires for reused connections
- Half-open TCP connections (remote closed, local doesn't know)
Root Cause
Socket-based timeouts have race conditions with keep-alive connections:
- Fresh connection:
socket.connecting === true, timer starts on 'connect' event ✓
- Reused connection:
socket.connecting === false, timer starts immediately BUT
the response callback may already be pending, causing a race
- Half-open sockets: TCP connection is open locally but remote closed silently.
Writes succeed but reads hang forever. Socket-level checks don't detect this.
Solution
Use request.setTimeout() instead of socket-based timers. This works at the HTTP layer
and fires reliably for both fresh and reused connections.
Pattern: Two-Phase Timeout
const TTFB_TIMEOUT = 8_000;
const BODY_TIMEOUT = 45_000;
const proxyReq = https.request(targetUrl, options, (proxyRes) => {
proxyReq.setTimeout(BODY_TIMEOUT);
proxyRes.pipe(res);
});
proxyReq.on('error', (err) => {
console.error('Request failed:', err.message);
});
proxyReq.setTimeout(TTFB_TIMEOUT, () => {
console.error('TTFB timeout - upstream unreachable');
proxyReq.destroy(new Error('TTFB timeout'));
});
proxyReq.write(body);
proxyReq.end();
What NOT to Do (Anti-Pattern)
proxyReq.on('socket', (socket) => {
if (socket.connecting) {
socket.setTimeout(CONNECT_TIMEOUT, () => { });
socket.once('connect', () => {
const ttfbTimer = setTimeout(() => { }, TTFB_TIMEOUT);
});
} else {
const ttfbTimer = setTimeout(() => { }, TTFB_TIMEOUT);
}
});
Verification
- Start upstream service, send request → should succeed
- Send second request immediately → should succeed (keep-alive reuse)
- Kill upstream, send request → should timeout in TTFB_TIMEOUT, not hang forever
- Restart upstream, send request → should succeed
Example: Complete Proxy Implementation
function tryProvider(targetUrl, body) {
return new Promise((resolve, reject) => {
let settled = false;
const proxyReq = https.request(targetUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
}, (proxyRes) => {
if (settled) return;
proxyReq.setTimeout(45_000);
settled = true;
resolve(proxyRes);
});
proxyReq.on('error', (err) => {
if (settled) return;
settled = true;
reject(err);
});
proxyReq.setTimeout(8_000, () => {
if (!settled) {
proxyReq.destroy(new Error('TTFB timeout'));
}
});
proxyReq.write(body);
proxyReq.end();
});
}
Notes
request.setTimeout() resets the timer on each data event, making it suitable for
both TTFB detection and stalled-body detection
- The
settled guard prevents double-resolution in timeout vs response races
- For circuit breaker patterns, combine with health probes that pre-emptively open
the circuit when the upstream is detected as down
- Keep-alive connections are the default in Node.js (via
http.Agent). The issue
manifests when the upstream dies while connections are pooled.
Related Patterns
- Health Probe: Run periodic checks against upstream to detect failures early
- Circuit Breaker: After N failures, skip the dead upstream entirely
- Connection Pool Tuning: Reduce
agent.keepAlive timeout to match upstream
References