Servers and environment

Shut a server down gracefully

Respond to termination signals by stopping new connections and allowing current work to finish before the process exits.

8 minute lesson

~~~

Production platforms send a termination signal before replacing or stopping a process.

On Unix-like platforms this is commonly SIGTERM; a terminal interruption is commonly SIGINT. Installing a signal listener replaces Node’s default exit behavior, so your handler must eventually let the process finish or mark failure.

Use one idempotent shutdown function:

let shuttingDown = false
let ready = true

async function shutdown(signal) {
  if (shuttingDown) return
  shuttingDown = true

  console.log(`Received ${signal}; draining`)
  ready = false

  const deadline = setTimeout(() => {
    console.error('Shutdown deadline exceeded')
    server.closeAllConnections()
    process.exit(1)
  }, 10_000)

  try {
    await new Promise((resolve, reject) => {
      server.close((error) => error ? reject(error) : resolve())
    })

    await database.close()
    clearTimeout(deadline)
  } catch (error) {
    console.error('Graceful shutdown failed', error)
    process.exitCode = 1
    // Keep the deadline active so the process cannot hang forever.
  }
}

process.on('SIGTERM', () => void shutdown('SIGTERM'))
process.on('SIGINT', () => void shutdown('SIGINT'))

Mark readiness false first so the load balancer stops sending traffic. server.close() stops accepting new connections and waits for active requests; on current Node releases it also closes idle keep-alive connections before completing. Close database, queue, telemetry, and other clients only after work that needs them has drained.

The deadline prevents one stuck request from hanging deployment forever. server.closeAllConnections() is forceful and can cut active responses, so reserve it for the deadline rather than the normal path. The immediate process.exit(1) is also deliberately abrupt here: the graceful window has already failed. Older supported Node versions can differ in how idle HTTP connections are reaped; check the version your service runs.

Do not call process.exit(0) immediately after receiving the signal. That defeats draining and can truncate logs or writes. When the server and resources no longer keep the event loop alive, Node exits naturally with the selected status.

Keep health checks honest during shutdown so traffic moves elsewhere.

Your action is to start one slow request, send SIGTERM, and prove that a new request is rejected while the existing one completes. Then create a deliberately stuck request and confirm the deadline forces a non-zero shutdown.

Lesson completed

Take this course offline

Get every free book and course as PDF and EPUB files.

Get the download library →