# Graceful shutdown on SIGTERM

> Stop accepting connections, finish in-flight requests, then exit with a deadline.

- Canonical: https://js-on-k8s.dev/recipes/graceful-shutdown
- Site: JavaScript on Kubernetes (https://js-on-k8s.dev)
- Updated: 2026-09-10
- Tags: signals, shutdown, http
- Example: https://github.com/vojtechmares/js-on-k8s/tree/main/examples/full/src/server.js
- Full example: https://github.com/vojtechmares/js-on-k8s/tree/main/examples/full

Without a `SIGTERM` handler, Node.js exits immediately and in-flight requests fail.
With a handler that never finishes, Kubernetes waits for `terminationGracePeriodSeconds`
and then kills the process anyway. Do both: handle the signal, and set a deadline.

## Handler

```js
import { createServer } from "node:http";

const server = createServer(handler);
server.listen(process.env.PORT ?? 3000);

let shuttingDown = false;

function shutdown(signal) {
  if (shuttingDown) return;
  shuttingDown = true;
  console.log(JSON.stringify({ level: "info", msg: `${signal} received, shutting down` }));

  // 1. Stop accepting new connections, finish in-flight requests.
  server.close((err) => process.exit(err ? 1 : 0));
  // 2. Close idle keep-alive connections so close() can finish.
  server.closeIdleConnections();
  // 3. Hard deadline, shorter than terminationGracePeriodSeconds.
  setTimeout(() => process.exit(1), 10_000).unref();
}

process.on("SIGTERM", shutdown);
process.on("SIGINT", shutdown);
```

## Pod spec

```yaml
spec:
  terminationGracePeriodSeconds: 30
  containers:
    - name: app
      lifecycle:
        preStop:
          sleep:
            seconds: 5
```

## Why the preStop sleep

Endpoint removal and `SIGTERM` happen in parallel. For a few seconds after the
signal, kube-proxy and ingress controllers may still route new requests to the Pod.
Sleeping in `preStop` delays the signal until routing has caught up. The `sleep`
action is available since Kubernetes 1.30; use `exec` with `sleep 5` on older clusters,
which requires a shell in the image.

## Notes

- Order of timeouts: `preStop` + app deadline < `terminationGracePeriodSeconds`.
- Also fail the readiness probe as soon as shutdown starts. See [health checks](/recipes/probes).
- Close database pools and flush logs after `server.close()` resolves, before `process.exit()`.
- Since Node.js 19, `server.close()` closes idle keep-alive connections itself. The explicit call keeps older versions working.
- Using a framework or a meta-framework? The recipes below cover Express, Fastify, Hono, Elysia, NestJS, Next.js and TanStack Start.

## In this section

- [Graceful shutdown with Express](https://js-on-k8s.dev/recipes/graceful-shutdown/express) - app.listen() returns a plain Node.js http.Server. Close that, not the app.
- [Graceful shutdown with Fastify](https://js-on-k8s.dev/recipes/graceful-shutdown/fastify) - Use app.close() with forceCloseConnections and onClose hooks, and listen on 0.0.0.0.
- [Graceful shutdown with Hono](https://js-on-k8s.dev/recipes/graceful-shutdown/hono) - Hono is runtime-agnostic. Close the server the adapter gave you, on Node or on Bun.
- [Graceful shutdown with Elysia](https://js-on-k8s.dev/recipes/graceful-shutdown/elysia) - Elysia runs on Bun. Call app.stop() on SIGTERM and ship it in the Bun distroless image.
- [Graceful shutdown with NestJS](https://js-on-k8s.dev/recipes/graceful-shutdown/nestjs) - Turn on enableShutdownHooks() and use the lifecycle hooks to flip readiness and close resources in order.
- [Graceful shutdown with Next.js](https://js-on-k8s.dev/recipes/graceful-shutdown/nextjs) - The standalone server already handles SIGTERM. Add readiness and cleanup around it, do not replace it.
- [Graceful shutdown with TanStack Start and Nitro](https://js-on-k8s.dev/recipes/graceful-shutdown/tanstack-start) - Nitro's node server drains in-flight requests on SIGTERM. Tune its timeout and add readiness in the server entry.
