Skip to content

Express Graceful Shutdown — Complete Implementation Guide

DodaTech Updated 2026-06-28 7 min read

In this tutorial, you will learn about Express Graceful Shutdown. We cover key concepts, practical examples, and best practices to help you master this topic.

Express.js graceful shutdown requires wrapping the server instance, tracking active requests, handling keep-alive connections, and ensuring middleware that holds resources gets cleaned up properly.

What You'll Learn

By the end of this tutorial, you will know how to implement graceful shutdown in Express.js applications, including request tracking, connection draining, and middleware cleanup.

Why It Matters

Express.js powers millions of Node.js applications. Without graceful shutdown, every deployment drops active requests, creating errors for users and potentially corrupting data.

Real-World Use

DodaTech's Express.js API Gateway handles 5000 requests per second. Its shutdown handler tracks every active request, sends 503 to new requests during drain, and waits for all in-flight requests to complete.

Express Graceful Shutdown Learning Path

flowchart LR
  A[Zero-Downtime Deploy] --> B[Express Graceful Shutdown]
  B --> C[Server Close]
  B --> D[Request Tracking]
  B --> E[Middleware Cleanup]
  B --> F{You Are Here}
  style F fill:#f90,color:#fff

Basic Express Graceful Shutdown

The simplest approach wraps the server and closes it on SIGTERM.

const express = require("express");

function createGracefulExpress() {
  const app = express();
  const server = require("http").createServer(app);

  app.get("/slow", async (req, res) => {
    await new Promise(r => setTimeout(r, 5000));
    res.json({ message: "Done" });
  });

  function shutdown() {
    console.log("Received SIGTERM, closing server");
    server.close(() => {
      console.log("Server closed, exiting");
      process.exit(0);
    });
  }

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

  return { app, server };
}

const { server } = createGracefulExpress();
server.listen(3000, () => console.log("Express listening on 3000"));

Tracking Active Requests

Track every request so the shutdown handler knows when all requests complete.

const express = require("express");

function createTrackedExpressApp() {
  const app = express();
  let activeRequests = 0;
  let shuttingDown = false;

  app.use((req, res, next) => {
    if (shuttingDown) {
      return res.status(503).json({ error: "Server shutting down" });
    }
    activeRequests++;
    res.on("finish", () => {
      activeRequests--;
      console.log(`Active requests: ${activeRequests}`);
    });
    next();
  });

  app.get("/api/data", (req, res) => {
    setTimeout(() => res.json({ data: "result" }), 2000);
  });

  app.get("/health/ready", (req, res) => {
    if (shuttingDown) {
      return res.status(503).json({ status: "not ready" });
    }
    res.json({ status: "ready", activeRequests });
  });

  return { app, getState: () => ({ activeRequests, shuttingDown }) };
}

const { app } = createTrackedExpressApp();
module.exports = app;

Shutdown with Promise Support

Using promises makes the shutdown sequence cleaner and composable.

const express = require("express");

class ExpressGracefulShutdown {
  constructor(app) {
    this.app = app;
    this.server = null;
    this.activeRequests = 0;
    this.shuttingDown = false;
    this.timeoutMs = 25000;
  }

  start(port) {
    this.server = this.app.listen(port);
    console.log(`Express listening on port ${port}`);

    process.on("SIGTERM", () => this.shutdown());
    process.on("SIGINT", () => this.shutdown());
  }

  async shutdown() {
    console.log("Graceful shutdown started");
    this.shuttingDown = true;

    await this.stopAcceptingRequests();
    await this.drainActiveRequests();
    await this.closeServer();
    await this.cleanupResources();

    console.log("Shutdown complete");
    process.exit(0);
  }

  async stopAcceptingRequests() {
    console.log("Rejecting new requests (503)");
    return Promise.resolve();
  }

  async drainActiveRequests() {
    if (this.activeRequests === 0) return;
    console.log(`Draining ${this.activeRequests} active requests...`);

    return new Promise((resolve) => {
      const start = Date.now();
      const check = () => {
        if (this.activeRequests === 0) {
          console.log("All requests drained");
          resolve();
        } else if (Date.now() - start > this.timeoutMs) {
          console.log(`Drain timeout, ${this.activeRequests} remaining`);
          resolve();
        } else {
          setTimeout(check, 200);
        }
      };
      check();
    });
  }

  async closeServer() {
    return new Promise((resolve) => {
      this.server.close(() => {
        console.log("HTTP server closed");
        resolve();
      });
    });
  }

  async cleanupResources() {
    console.log("Cleaning up resources");
  }
}

const app = express();
const graceful = new ExpressGracefulShutdown(app);
graceful.start(3000);

Integration with Express Middleware

Some middleware holds resources that must be released during shutdown.

class SessionMiddlewareShutdown {
  constructor(sessionStore) {
    this.sessionStore = sessionStore;
  }

  middleware() {
    return (req, res, next) => {
      next();
    };
  }

  async shutdown() {
    if (this.sessionStore && this.sessionStore.close) {
      console.log("Closing session store");
      await this.sessionStore.close();
    }
  }
}

function createShutdownAwareMiddleware() {
  const middlewares = [];
  let shuttingDown = false;

  const apiKeyAuth = (req, res, next) => {
    if (shuttingDown) {
      return res.status(503).json({ error: "Shutting down" });
    }
    next();
  };

  const requestLogger = (req, res, next) => {
    console.log(`${req.method} ${req.path}`);
    res.on("finish", () => {
      console.log(`${res.statusCode} ${req.path}`);
    });
    next();
  };

  return {
    middlewares: [apiKeyAuth, requestLogger],
    shutdown: () => { shuttingDown = true; }
  };
}

const mw = createShutdownAwareMiddleware();
const expressApp = express();
mw.middlewares.forEach(m => expressApp.use(m));

Graceful Shutdown with Express Cluster

In cluster mode, each worker handles its own shutdown.

const cluster = require("cluster");
const express = require("express");
const os = require("os");

function createClusterApp() {
  if (cluster.isPrimary) {
    console.log(`Primary ${process.pid} running`);
    const workers = [];

    for (let i = 0; i < os.cpus().length; i++) {
      const worker = cluster.fork();
      workers.push(worker);
    }

    process.on("SIGTERM", () => {
      console.log("Primary received SIGTERM, forwarding to workers");
      workers.forEach(w => w.kill("SIGTERM"));
    });
  } else {
    const app = express();
    const server = app.listen(3000);

    app.get("/", (req, res) => res.json({ worker: process.pid }));

    process.on("SIGTERM", () => {
      console.log(`Worker ${process.pid} shutting down`);
      server.close(() => process.exit(0));
    });

    console.log(`Worker ${process.pid} started`);
  }
}

// createClusterApp() would start the cluster

Common Mistakes

  1. Not calling server.close() before Process.exit() -- process.exit() terminates immediately. Always call server.close() first and wait for the callback.

  2. Using app.close() instead of server.close() -- Express app doesn't have a close method. Use the HTTP server instance returned by app.listen().

  3. Not handling keep-alive connections -- HTTP keep-alive connections stay open. server.close() waits for them. Set keepAliveTimeout to a reasonable value.

  4. Blocking the event loop in shutdown handlers -- Don't use synchronous operations during shutdown. The event loop needs to process remaining requests.

  5. Registering SIGTERM handlers before the server starts -- If registration happens before server.listen(), a SIGTERM during startup closes a server that hasn't started yet.

Practice Questions

  1. How do you get the HTTP server instance in Express? app.listen() returns an http.Server instance. Store it in a variable and use it for server.close().

  2. What is the purpose of tracking active requests during shutdown? To know when all in-flight requests have completed so the server can exit without dropping any responses.

  3. How do you handle keep-alive connections in Express shutdown? Set server.keepAliveTimeout to a low value (e.g., 5000ms) so idle keep-alive connections close quickly during drain.

  4. Challenge: Implement an Express middleware that adds a shutdown warning header to all responses when shutdown is in progress.

function shutdownWarningMiddleware(shutdownState) {
  return (req, res, next) => {
    if (shutdownState.active) {
      res.setHeader("X-Shutdown-Warning", "true");
      res.setHeader("X-Shutdown-Remaining", shutdownState.getRemaining());
    }
    next();
  };
}

const shutdownState = {
  active: false,
  startTime: null,
  getRemaining() {
    if (!this.active) return "0";
    return Math.max(0, 25000 - (Date.now() - this.startTime)) + "ms";
  },
  begin() {
    this.active = true;
    this.startTime = Date.now();
  }
};

const app = require("express")();
app.use(shutdownWarningMiddleware(shutdownState));
app.get("/api", (req, res) => res.json({ ok: true }));

FAQ

Can I use Express's built-in shutdown support?

Express doesn't have built-in shutdown support. You must implement it using the HTTP server instance and process signal handlers.

How do I handle file uploads during shutdown?

Let the current upload complete, then reject new uploads. Use busboy or multer events to track upload progress.

Should I close database connections in the Express shutdown handler?

Yes. Close database pools, Redis connections, and other resources after all requests are drained.

What Express version supports the best shutdown patterns?

Express 4.x and 5.x both work. The key is the HTTP server instance, which hasn't changed between versions.

How do I test Express shutdown behavior?

Write integration tests that start the server, make requests, trigger SIGTERM, and verify all responses complete successfully.

Mini Project

Build a complete Express.js application with graceful shutdown including request tracking, connection draining, database pool closing, health check integration, and cluster support.

const express = require("express");

class ProductionExpressApp {
  constructor() {
    this.app = express();
    this.server = null;
    this.state = { active: 0, shuttingDown: false, startTime: null };
    this.setupMiddleware();
    this.setupRoutes();
  }

  setupMiddleware() {
    this.app.use((req, res, next) => {
      if (this.state.shuttingDown) {
        return res.status(503).json({ error: "shutting down" });
      }
      this.state.active++;
      res.on("finish", () => this.state.active--);
      next();
    });
  }

  setupRoutes() {
    this.app.get("/", (req, res) => res.json({ status: "ok" }));
    this.app.get("/health", (req, res) => {
      res.json({
        healthy: !this.state.shuttingDown,
        activeRequests: this.state.active
      });
    });
  }

  start(port) {
    this.server = this.app.listen(port, () => {
      console.log(`Server on port ${port}`);
    });
    process.on("SIGTERM", () => this.shutdown());
  }

  async shutdown() {
    console.log("Shutdown initiated");
    this.state.shuttingDown = true;
    this.state.startTime = Date.now();

    await new Promise(r => setTimeout(r, 1000));
    this.server.close(() => {
      console.log("Server closed");
      process.exit(0);
    });
  }
}

const app = new ProductionExpressApp();
app.start(3000);

What's Next

Now that you understand Express graceful shutdown, build the complete graceful shutdown project that combines all concepts into a production-ready implementation.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro