Zero-Downtime Node.js Deployments: A Guide

September 16, 2026 · 3 views
Zero-Downtime Node.js Deployments: A Guide

Every deploy that restarts your Node.js process the naive way drops requests. Users see a connection reset for a few seconds, the health check fails, and if you're deploying multiple times a day that's a few seconds of downtime, several times a day, forever. Zero-downtime Node.js deployments aren't complicated to set up — they just require the process to shut down gracefully and the load balancer to know when it's actually ready, not just running.

Why a Simple Restart Causes Downtime

A basic pm2 restart or systemctl restart kills the process immediately and starts a new one. Between the kill and the new process binding to its port, any in-flight request gets dropped, and any new request arriving in that window hits a closed socket. It's a small window, but it's a real one, and it compounds with deployment frequency — daily deploys mean daily user-visible errors if nobody addresses this.

Graceful Shutdown: Let In-Flight Requests Finish

The first piece is making your app actually listen for a shutdown signal and finish what it's doing before exiting, instead of dying instantly:

const server = app.listen(3000);

let isShuttingDown = false;

process.on("SIGTERM", () => {
  if (isShuttingDown) return;
  isShuttingDown = true;

  console.log("SIGTERM received, draining connections...");

  server.close(() => {
    console.log("All connections drained, exiting.");
    process.exit(0);
  });

  // Force-exit if draining takes too long
  setTimeout(() => {
    console.error("Forced shutdown after timeout.");
    process.exit(1);
  }, 10_000);
});

server.close() stops accepting new connections but lets existing ones complete — that's the core mechanism. The timeout guards against a hung connection blocking shutdown forever. Your process manager (PM2, systemd, Kubernetes) needs to actually send SIGTERM and wait before force-killing with SIGKILL — most do this by default, but it's worth confirming your deployment tooling isn't sending SIGKILL immediately.

Readiness Checks: Don't Route Traffic to a Process That Isn't Ready

Graceful shutdown handles the exit side; the start side needs the same care. A new process might be running but not actually ready — still connecting to the database, still warming a cache. Route traffic to it too early and requests fail against a half-initialized app.

let isReady = false;

app.get("/health/ready", (req, res) => {
  if (!isReady) return res.status(503).send("not ready");
  res.status(200).send("ready");
});

async function start() {
  await db.connect();
  await cache.warm();
  isReady = true;
  app.listen(3000);
}

Your load balancer or orchestrator should poll /health/ready and only route traffic once it returns 200 — this is standard in Kubernetes as a readiness probe, and just as achievable with a basic Nginx/HAProxy health check on a traditional VPS setup.

Rolling Restarts: Never Take Every Instance Down at Once

If you run more than one instance behind a load balancer, restart them one at a time, not all simultaneously. PM2's cluster mode supports this natively:

pm2 reload ecosystem.config.js --update-env

pm2 reload (not restart) restarts cluster workers one at a time, keeping at least one instance serving traffic throughout. On Kubernetes, a standard rolling update Deployment strategy does the same thing by default — it just needs the readiness probe above to actually know when a new pod is safe to receive traffic.

Database Migrations: The Part People Forget

Zero-downtime deploys break silently when a migration changes the schema in a way old and new code can't both handle during the rollout window. The fix is backward-compatible migrations: add a new column as nullable first, deploy code that writes to both old and new columns, backfill, then remove the old column in a later deploy — never a single migration that both adds a required column and drops the old one in the same deploy that also ships code depending on it.

Common Mistakes Teams Make

  • Ignoring SIGTERM entirely. Without a handler, the process manager waits out its grace period and then SIGKILLs — same as an instant kill, just delayed.
  • No readiness check, only a liveness check. A process can be "alive" (not crashed) while still unready to serve real traffic — these are different questions and need different endpoints.
  • Restarting all instances in parallel to save time. Faster deploys aren't worth user-visible downtime; rolling restarts cost a few extra seconds, not minutes.
  • Migrations that assume the deploy is instantaneous. During a rolling restart, old and new code run simultaneously for a window — schema changes need to tolerate that.

Best Practices

Set your shutdown timeout realistically — long enough for real requests to finish, short enough that a genuinely hung connection doesn't block deploys indefinitely; 10-30 seconds is typical for most web APIs. Log shutdown events explicitly so a slow or failed graceful shutdown shows up in your monitoring instead of silently degrading into hard kills. And test the actual deploy process under load occasionally, not just in a quiet staging environment — connection draining behaves differently when there's real concurrent traffic to drain.

Frequently Asked Questions

Does this work with a single server, no load balancer? Graceful shutdown still helps — it prevents dropped in-flight requests during the restart window — but you can't avoid a brief unavailable window with only one instance. True zero-downtime requires at least two instances so one can serve while the other restarts.

What's the difference between a liveness and readiness check? Liveness answers "is the process running and not deadlocked" — failing it should trigger a restart. Readiness answers "should traffic be routed here right now" — failing it should just pull the instance out of rotation, not restart it.

Does this apply to serverless/Lambda deployments? Less directly — serverless platforms handle instance lifecycle differently, and cold starts are a separate concern from this. This guide is specifically for long-running Node.js processes (traditional servers, containers, PM2/Kubernetes deployments).

Key Takeaways

Zero-downtime Node.js deployments come down to three pieces working together: graceful shutdown that drains in-flight requests instead of dropping them, a readiness check that keeps traffic away from instances that aren't actually ready, and rolling restarts that never take every instance down at once. None of these require exotic infrastructure — PM2 cluster mode or a basic Kubernetes Deployment both support this out of the box once the application code itself handles SIGTERM and exposes a real readiness endpoint.

#nodejs #devops #zero-downtime #kubernetes #deployment
Share this article: