Skip to content

Draining Connections — Complete Implementation Guide

DodaTech Updated 2026-06-28 6 min read

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

Connection draining is the Process of allowing active connections to complete their work while preventing new connections, ensuring no data is lost and no client is disconnected abruptly.

What You'll Learn

By the end of this tutorial, you will know how to drain HTTP connections, Websocket connections, and database connection pools during a graceful shutdown sequence.

Why It Matters

Connections carry active requests. Draining them improperly causes client-side errors, data corruption, and database connection leaks. Proper draining ensures every in-flight operation completes.

Real-World Use

DodaZIP's WebSocket server maintains 10,000 concurrent file upload connections. During shutdown, it signals clients to reconnect, drains each connection as uploads complete, and closes the server only after the last upload finishes.

Connection Draining Learning Path

flowchart LR
  A[SIGTERM and SIGINT] --> B[Draining Connections]
  B --> C[HTTP Draining]
  B --> D[WebSocket Draining]
  B --> E[DB Pool Draining]
  B --> F{You Are Here}
  style F fill:#f90,color:#fff

Draining HTTP Connections

Node.js http.Server.close() stops accepting new connections and waits for existing keep-alive connections to close.

const http = require("http");

function createDrainableServer() {
  const server = http.createServer((req, res) => {
    setTimeout(() => {
      res.end("Response");
    }, 2000);
  });

  let shuttingDown = false;

  function drainConnections() {
    shuttingDown = true;
    console.log("Stopping accepting new connections");

    server.close(() => {
      console.log("All connections drained, server closed");
    });

    setTimeout(() => {
      console.log("Force closing remaining connections");
      server.closeAllConnections();
    }, 10000).unref();
  }

  server.listen(3000, () => console.log("Listening"));
  return { server, drainConnections };
}

const app = createDrainableServer();
// Trigger shutdown after 1 second
setTimeout(() => app.drainConnections(), 1000);
// Stopping accepting new connections
// (existing requests complete)
// All connections drained, server closed

Tracking Active Connections

For finer control, track active connections and drain them individually.

const http = require("http");

class ConnectionTracker {
  constructor() {
    this.activeConnections = new Set();
    this.requestCount = 0;
  }

  track(socket) {
    this.activeConnections.add(socket);
    socket.on("close", () => {
      this.activeConnections.delete(socket);
    });
  }

  async drain(timeoutMs = 10000) {
    console.log(`Draining ${this.activeConnections.size} active connections`);
    const start = Date.now();

    return new Promise((resolve) => {
      const check = () => {
        if (this.activeConnections.size === 0) {
          console.log("All connections drained");
          resolve();
        } else if (Date.now() - start > timeoutMs) {
          console.log(`Timeout: ${this.activeConnections.size} connections remaining`);
          this.activeConnections.forEach(socket => socket.destroy());
          resolve();
        } else {
          setTimeout(check, 100);
        }
      };
      check();
    });
  }
}

const tracker = new ConnectionTracker();
const server = http.createServer((req, res) => {
  tracker.track(req.socket);
  setTimeout(() => res.end("ok"), 1000);
});

server.listen(3000);
console.log("Server with connection tracking started");
// Server with connection tracking started

Draining WebSocket Connections

WebSocket connections require signaling the client before closing.

const WebSocket = require("ws");

class WebSocketDrainer {
  constructor(wss) {
    this.wss = wss;
    this.clients = new Set();

    wss.on("connection", (ws) => {
      this.clients.add(ws);
      ws.on("close", () => this.clients.delete(ws));
    });
  }

  async drain(timeoutMs = 15000) {
    console.log(`Draining ${this.clients.size} WebSocket connections`);

    this.clients.forEach(client => {
      client.send(JSON.stringify({
        type: "shutdown",
        message: "Server restarting, please reconnect",
        reconnectIn: 5000
      }));
    });

    const start = Date.now();
    return new Promise((resolve) => {
      const check = () => {
        if (this.clients.size === 0) {
          console.log("All WebSocket connections drained");
          resolve();
        } else if (Date.now() - start > timeoutMs) {
          console.log(`Force closing ${this.clients.size} remaining WebSocket connections`);
          this.clients.forEach(client => client.terminate());
          resolve();
        } else {
          setTimeout(check, 200);
        }
      };
      check();
    });
  }
}

const wss = new WebSocket.Server({ port: 3001 });
const drainer = new WebSocketDrainer(wss);
console.log("WebSocket server with drain support started");
// WebSocket server with drain support started

Draining Database Connection Pools

Database pools must drain their connections to avoid leaving open connections that accumulate over time.

class DatabasePoolDrainer {
  constructor(pool) {
    this.pool = pool;
  }

  async drain() {
    const idle = this.pool.idleConnections?.() || 0;
    const total = this.pool.totalConnections?.() || 0;
    console.log(`Pool status: ${idle} idle / ${total} total`);

    // Stop accepting new connections from the pool
    this.pool.options.max = 0;

    // Wait for active queries to complete
    const waitTime = 5000;
    const start = Date.now();

    while (Date.now() - start < waitTime) {
      const active = (this.pool.totalConnections?.() || 0) -
                     (this.pool.idleConnections?.() || 0);
      if (active === 0) {
        console.log("All pool connections released");
        break;
      }
      console.log(`Waiting for ${active} active queries to complete...`);
      await new Promise(r => setTimeout(r, 200));
    }

    // End the pool
    await this.pool.end();
    console.log("Database pool closed");
  }
}

const { Pool } = require("pg");
const pool = new Pool({ max: 10 });
const dbDrainer = new DatabasePoolDrainer(pool);
dbDrainer.drain();
// Pool status: 0 idle / 0 total
// Database pool closed

Common Mistakes

  1. Calling server.close() before stopping the health check -- Connections keep arriving while draining if the health check still reports healthy. Set health to unhealthy first.

  2. Not draining WebSocket connections before HTTP server -- WebSocket connections are not HTTP connections. The HTTP server may close before WebSocket draining completes. Drain WebSockets first.

  3. Ignoring keep-alive connections -- HTTP keep-alive connections stay open indefinitely. server.close() waits for them to close, but they never will without explicit timeout.

  4. Destroying connections without signaling -- Abruptly destroying TCP connections causes ECONNRESET on the client. Always send a shutdown message first for WebSocket and long-poll connections.

  5. Not setting a maximum drain time -- A stuck connection can prevent shutdown indefinitely. Always set a timeout and force-close remaining connections after it expires.

Practice Questions

  1. What does server.close() do in Node.js? It stops the server from accepting new connections and keeps the server running until all existing connections are closed.

  2. How do you force-close remaining connections after a timeout? Call server.closeAllConnections() (Node.js 18+) or iterate through sockets and call socket.destroy().

  3. Why must WebSocket connections be drained differently from HTTP? WebSocket connections are long-lived bidirectional channels. Clients need a signal to reconnect gracefully. HTTP connections are typically short-lived.

  4. Challenge: Implement a drainer that shows a live countdown of remaining connections.

class LiveConnectionDrainer {
  constructor(server) {
    this.server = server;
    this.connections = new Set();
  }

  startTracking() {
    this.server.on("connection", (socket) => {
      this.connections.add(socket);
      socket.on("close", () => {
        this.connections.delete(socket);
        this.updateDisplay();
      });
    });
  }

  updateDisplay() {
    process.stdout.write(`\rActive connections: ${this.connections.size}`);
  }

  async drain() {
    return new Promise((resolve) => {
      this.server.close(() => {
        this.updateDisplay();
        console.log("\nServer closed");
        resolve();
      });
    });
  }
}

const server = http.createServer((req, res) => res.end("ok"));
const drainer = new LiveConnectionDrainer(server);
drainer.startTracking();
server.listen(3000);

FAQ

How long should I wait for connections to drain?

5-10 seconds for HTTP connections, 15-30 seconds for WebSocket connections. Match your application's maximum expected request duration.

Do I need to drain connections in development?

It's a good practice. Connection draining code is only useful and tested if used consistently in all environments.

What happens to connections that don't drain in time?

They should be force-closed by destroying the socket. This causes an error on the client but is better than leaving connections hanging indefinitely.

How do I handle HTTP/2 connections?

HTTP/2 multiplexes multiple requests over one connection. Use http2 server.close() similarly, but note that a single client can have many active streams.

Should I drain in reverse order of creation?

Drain in dependency order: stop accepting requests first, then drain WebSocket connections, then HTTP connections, then database pools.

Mini Project

Build a connection drainer that tracks HTTP, WebSocket, and database connections with a unified drain interface and real-time progress display.

class UnifiedDrainer {
  constructor() {
    this.drainables = [];
  }

  add(name, drainFn) {
    this.drainables.push({ name, drain: drainFn });
  }

  async drainAll(timeoutMs = 15000) {
    console.log("Starting unified drain");
    for (const { name, drain } of this.drainables) {
      console.log(`Draining: ${name}`);
      await Promise.race([
        drain(),
        new Promise((_, reject) =>
          setTimeout(() => reject(new Error(`${name} drain timeout`)), timeoutMs)
        )
      ]);
      console.log(`Drained: ${name}`);
    }
    console.log("All drains complete");
  }
}

const drainer = new UnifiedDrainer();
drainer.add("http", () => new Promise(r => setTimeout(r, 1000)));
drainer.add("websocket", () => new Promise(r => setTimeout(r, 2000)));
drainer.add("database", () => new Promise(r => setTimeout(r, 500)));
drainer.drainAll();
// Starting unified drain
// Draining: http
// Drained: http
// Draining: websocket
// Drained: websocket
// Draining: database
// Drained: database
// All drains complete

What's Next

Now that you understand connection draining, learn how to handle in-flight requests during shutdown. Then explore closing database connection pools.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro