Draining Connections — Complete Implementation Guide
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
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.
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.
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.
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.
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
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.
How do you force-close remaining connections after a timeout? Call server.closeAllConnections() (Node.js 18+) or iterate through sockets and call socket.destroy().
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.
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
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