Graceful Shutdown

On SIGTERM, stop accepting new connections, let in-flight requests finish, close the DB, then exit — with a hard timeout as a backstop.

When your platform deploys or scales down, it sends your process SIGTERM and starts a countdown. If you ignore it, the platform eventually SIGKILLs you mid-request — dropped responses, half-written transactions, angry users. A graceful shutdown handles this window deliberately.

The choreography

  1. Catch SIGTERM (and SIGINT for local Ctrl-C).
  2. Call server.close() — it stops accepting new connections but lets in-flight requests drain.
  3. Close your database pool and other resources after the server has drained.
  4. Arm a hard timeout: if draining hangs, force-exit so you do not get SIGKILLed uncleanly.
process.on('SIGTERM', () => {
  server.close(() => { db.end(); process.exit(0); });
  setTimeout(() => process.exit(1), 10_000).unref();
});

Example

Example · javascript
import express from 'express';

const app = express();
let shuttingDown = false;

// Readiness probe flips first so the LB drains traffic before we close.
app.get('/health/ready', (req, res) => {
  res.status(shuttingDown ? 503 : 200).json({ ready: !shuttingDown });
});

app.get('/health/live', (req, res) => res.json({ live: true }));

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

async function shutdown(signal) {
  console.log(`${signal} received — draining`);
  shuttingDown = true; // /health/ready now returns 503

  // Backstop: if draining hangs, exit anyway before the platform SIGKILLs us.
  const hardStop = setTimeout(() => {
    console.error('Drain timed out, forcing exit');
    process.exit(1);
  }, 10_000);
  hardStop.unref();

  server.close(async () => {
    try {
      await db.end();      // close pools only AFTER requests have drained
      clearTimeout(hardStop);
      process.exit(0);
    } catch {
      process.exit(1);
    }
  });
}

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

When to use it

  • A Kubernetes deployment sends SIGTERM before terminating a pod; the Express app intercepts it to stop accepting connections and drain in-flight requests.
  • A developer stores the server handle returned by app.listen() to call server.close() on shutdown signals, preventing new connections while existing ones finish.
  • A database connection pool is explicitly closed in the SIGTERM handler so no queries are left open when the process exits.

More examples

Basic SIGTERM graceful shutdown

Intercepts SIGTERM, calls server.close() to stop accepting new connections, and exits once in-flight requests complete.

Example · js
const app = require('./app');
const server = app.listen(process.env.PORT || 3000, () => console.log('Started'));

process.on('SIGTERM', () => {
  console.log('SIGTERM received — shutting down');
  server.close(() => {
    console.log('All connections closed');
    process.exit(0);
  });
});

Drain connections with timeout

Sets a forced exit timeout of 10 seconds as a safety net, closes the HTTP server, then drains the database pool.

Example · js
async function shutdown(server) {
  console.log('Shutting down...');
  await new Promise((resolve, reject) => {
    server.close(err => (err ? reject(err) : resolve()));
  });
  await db.pool.end(); // close DB connections
  console.log('Clean exit');
  process.exit(0);
}

const FORCE_EXIT_MS = 10_000;
process.on('SIGTERM', () => {
  setTimeout(() => process.exit(1), FORCE_EXIT_MS).unref();
  shutdown(server);
});

Handle SIGINT and SIGTERM

Registers the same graceful shutdown logic for both SIGTERM (from Kubernetes) and SIGINT (from Ctrl+C in development).

Example · js
function graceful(signal) {
  return () => {
    console.log(`${signal} received`);
    server.close(() => {
      console.log('Server closed');
      process.exit(0);
    });
  };
}

process.on('SIGTERM', graceful('SIGTERM'));
process.on('SIGINT',  graceful('SIGINT')); // Ctrl+C in dev

Discussion

  • Be the first to comment on this lesson.