forceCloseConnections: 'idle' default is unreachable, gated on options.serverFactory
Prerequisites
- I have written a descriptive issue title
- I have searched existing issues to ensure it has not already been reported
Fastify version
5.12.5
Node.js version
v26.7.0
Operating system
macOS (Darwin 25.6.0, arm64)
Description
forceCloseConnections defaults to 'idle' on any Node version exposing closeIdleConnections,
but the branch implementing 'idle' is gated on options.serverFactory, which is undefined unless
the user supplies one. So the default value can never take effect on a default server.
The visible consequence is that close() waits out the full keepAliveTimeout — 72,000ms by
Fastify's default — for a keep-alive connection that becomes idle after shutdown has begun.
Steps to Reproduce
import Fastify from 'fastify'
import http from 'node:http'
const app = Fastify() // vary the options here
app.get('/slow', async () => {
await new Promise((r) => setTimeout(r, 1500))
return 'ok'
})
await app.listen({ port: 3000, host: '0.0.0.0' })
const agent = new http.Agent({ keepAlive: true })
http.get({ host: '127.0.0.1', port: 3000, path: '/slow', agent }, (r) => r.resume())
setTimeout(async () => {
const t = Date.now()
await app.close()
console.log(`close() resolved after ${Date.now() - t}ms`)
}, 400)Measured, only the option varying:
| options | close() resolved |
|---|---|
{} (default, i.e. 'idle') |
74,111ms |
{ forceCloseConnections: 'idle' } |
74,113ms |
{ forceCloseConnections: true } |
4ms — destroys the in-flight request |
bare node:http, for comparison |
7,110ms (same shape, 5,000ms timeout) |
Expected Behavior
With 'idle' — whether defaulted or passed explicitly — I would expect idle keep-alive connections
to be closed during shutdown, so close() resolves shortly after the last in-flight request
finishes rather than after keepAliveTimeout.
Possible cause
lib/server.js (v5.12.5) sets the default:
} else if (typeof forceCloseConnections !== 'boolean') {
/* istanbul ignore next: only one branch can be valid in a given Node.js version */
forceCloseConnections = serverHasCloseIdleConnections ? 'idle' : false
}and fastify.js gates the behaviour on a user-supplied server factory:
/* istanbul ignore next: Cannot test this without Node.js core support */
if (forceCloseConnections === 'idle' && options.serverFactory) {
instance.server.closeIdleConnections()With no serverFactory the condition is false, so nothing calls closeIdleConnections(). Node's
own server.close() reaps only connections that are already idle when it is called, which is why a
socket going idle mid-shutdown is never collected.
Both lines carry istanbul ignore comments, so I do not think this path is covered.
Workaround
Reaping while the close is in progress drains correctly — 1,129ms, handler completes, client receives 200:
const closing = app.close()
const reap = setInterval(() => app.server.closeIdleConnections(), 50)
await closing
clearInterval(reap)Suggestion
Either drop the options.serverFactory condition so the documented default can take effect, or
state in the forceCloseConnections documentation that neither 'idle' nor the default reaps a
connection that becomes idle during shutdown, and that a graceful shutdown therefore needs a
timeout of its own. I would be glad to send a PR for whichever you prefer.
Source: fastify/fastify