#7044·fastify

forceCloseConnections: 'idle' default is unreachable, gated on options.serverFactory

Author: oolothCreated Sep 22, 2026Updated Sep 22, 2026

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

javascript
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:

javascript
} 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:

javascript
/* 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:

javascript
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.