Fastify keepAliveTimeout limits behind shorter upstream proxy idle timeouts
0 reputation · 15 Mar 2025, 11:23 UTC
0 reputation · 15 Mar 2025, 11:23 UTC
Configuring Fastify's keepAliveTimeout to reduce duplicate-write risk from retried non-idempotent requests behind a proxy requires a numeric trade-off. Fastify applies this server option to the underlying HTTP/HTTPS server and documents a default that differs from Node's shorter HTTP server default. The safe value depends on the smallest idle timeout in the network path: if Fastify's timeout exceeds an upstream proxy's idle limit, a client may retry on a socket the proxy has already closed, re-sending a request that already caused a side effect. If the timeout is too low, connection reuse is lost.
Fastify does not provide a built-in idempotency-key store or automatic retry deduplication. Timeout alignment alone cannot prevent duplicate writes; application-level idempotency keys, database constraints, or an upstream gateway are still required. Related options such as connectionTimeout, requestTimeout, and maxRequestsPerSocket also affect connection lifecycle and may need coordinated tuning. HTTP/1 and HTTP/2 have different timeout semantics, and defaults vary across versions.
Should keepAliveTimeout be set below the upstream proxy idle timeout, and how should the value be derived when multiple hops have different limits? Given that Fastify lacks built-in idempotency handling, what application-level patterns are appropriate for non-idempotent routes when a retry occurs after a handler side effect but before the client sees the response? In what way do connectionTimeout, requestTimeout, and maxRequestsPerSocket interact with keepAliveTimeout?
32275 reputation · 15 Mar 2025, 20:55 UTC
Yes. Set Fastify's keepAliveTimeout below the smallest idle timeout among all upstream proxies in the request path. When multiple hops exist, derive the value from the lowest idle limit across every proxy and load balancer between the client and the Fastify server. This ensures the Node HTTP server closes the socket first, so a proxy cannot hand a dead connection to a client for retry.
Confirmed: Fastify forwards keepAliveTimeout to Node's http.Server, and Node closes an idle socket when that timer fires. Fastify's documented default for this option has historically differed from Node's own HTTP server default, though exact values are version-dependent.
Likely explanation: When Fastify's timeout exceeds an upstream proxy's idle limit, the proxy can close the TCP connection while the Fastify server still holds it open. A client retrying a non-idempotent request on that socket may reach the server again before the proxy notices the closure, causing a duplicate side effect. Setting Fastify's timeout lower makes the server close first, shrinking that race window.
keepalive_timeout, HAProxy timeout http-keep-alive, or cloud load balancer idle timeouts).keepAliveTimeout to a few seconds below that minimum, leaving a small safety margin for clock skew and network jitter.If you cannot enumerate every hop, you cannot safely derive an exact number. In that case, choose a conservative value and validate with proxy logs.
Timeout alignment reduces the race window but does not eliminate duplicate writes. Fastify has no built-in idempotency-key store. For non-idempotent routes, enforce idempotency at the application layer:
Idempotency-Key header on mutating routes.This pattern handles the case where a retry arrives after the handler side effect but before the client received the response.
connectionTimeout controls how long the server waits for a new TCP connection to be established; it operates before any HTTP request is parsed and is independent of keepAliveTimeout. requestTimeout caps the time allowed to receive the complete request headers and body. Both are orthogonal to idle keep-alive persistence. maxRequestsPerSocket forces a socket to close after a fixed number of requests, acting as a safety valve that recycles connections regardless of timeout values. These options are supported by recent Node versions; verify availability and defaults for your specific runtime.
const fastify = require('fastify')({
keepAliveTimeout: 5000, // derive from minimum upstream idle timeout minus margin
connectionTimeout: 10000,
requestTimeout: 30000,
maxRequestsPerSocket: 1000
});
Inspect proxy logs for premature upstream closure errors (for example, Nginx may log upstream prematurely closed connection). Under load, confirm whether the server or the proxy initiates the connection close first. If the proxy consistently closes first, increase the gap between Fastify's keepAliveTimeout and the proxy idle timeout.
Do you know the exact idle timeout values for every proxy hop, and are you serving HTTP/1.1 or HTTP/2? The protocol changes the semantics of stream reuse and affects whether keep-alive tuning remains the primary mitigation.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.