Zero-Downtime Deployment for Node.js

Khimananda Oli 8 min read Programming and Languages
Zero-Downtime Deployment for Node.js

By Khimananda Oli | Last reviewed: August 2026

Dropped requests during releases are rarely a platform failure; they are usually an application lifecycle failure. Achieving zero-downtime deployment for Node.js requires coordinating three distinct layers: the application’s signal handling, the process manager’s restart strategy, and the load balancer’s health checks. If any one of these is misconfigured, users will see 502 Bad Gateway errors regardless of how sophisticated your infrastructure is. This guide breaks down the exact implementation required to make deployments invisible.

Load BalancerNode Instance A(Draining)Node Instance B(Active)New Pod(Starting)Shared State / DatabaseConnection Pool DrainingOrchestrator (K8s / PM2 / Systemd)Manages Rolling Update + Health Probes
Architecture overview for zero-downtime deployment for Node.js showing traffic flow during rolling updates

How do you implement graceful shutdown in Node.js?

The foundation of zero-downtime deployment for Node.js is the application's ability to stop accepting new work while finishing existing work. Node.js does not do this automatically. When a container orchestrator or process manager sends a SIGTERM signal, the default behavior is immediate termination, which severs active TCP connections and drops in-flight requests.

Catching signals and draining connections

You must explicitly listen for termination signals and coordinate the shutdown sequence. The following pattern works for Express, Fastify, and native HTTP servers. It stops the server from accepting new connections, waits for active requests to complete, and then closes database pools before exiting.

const server = app.listen(PORT, () => {
  console.log(`Server listening on port ${PORT}`);
});

const gracefulShutdown = async (signal) => {
  console.log(`${signal} received. Starting graceful shutdown...`);
  
  // Stop accepting new connections immediately
  server.close(async (err) => {
    if (err) {
      console.error('Error during server close:', err);
      process.exit(1);
    }
    
    console.log('HTTP server closed. Draining database connections...');
    
    // Close database pools, message queues, Redis clients
    try {
      await dbPool.end();
      await redisClient.quit();
      console.log('All resources drained. Exiting cleanly.');
      process.exit(0);
    } catch (cleanupErr) {
      console.error('Cleanup failed:', cleanupErr);
      process.exit(1);
    }
  });
  
  // Force exit if graceful shutdown takes too long
  setTimeout(() => {
    console.error('Forced shutdown: graceful timeout exceeded');
    process.exit(1);
  }, 25000); // Must be less than K8s terminationGracePeriodSeconds
};

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

A common mistake is setting the forced-shutdown timeout longer than your orchestrator’s kill grace period. In Kubernetes, if terminationGracePeriodSeconds is 30 and your app waits 35 seconds, Kubernetes sends SIGKILL at second 30, bypassing your cleanup entirely. Always set the application timeout to at least 5 seconds less than the infrastructure limit.

Handling long-lived connections

If your Node.js application serves WebSockets, SSE streams, or long-polling endpoints, server.close() alone is insufficient because it only stops new HTTP requests but leaves existing sockets open indefinitely. You must track active sockets and destroy them after a reasonable drain period, optionally sending a close frame to allow clients to reconnect gracefully.

  • Track all socket connections in a Set on the connection event
  • Remove sockets from the Set on the close event
  • During shutdown, iterate remaining sockets and call socket.destroy() after allowing in-flight messages to flush
  • For WebSocket servers, send a close code 1001 (Going Away) before destroying

For teams managing complex stateful connections, understanding the underlying runtime behavior is critical. My guide on installing and configuring Node.js on Ubuntu covers runtime-level tuning that complements application-level shutdown logic.

How do rolling updates prevent downtime in Kubernetes?

Graceful shutdown handles the application layer, but the orchestration layer determines whether traffic reaches a dying instance. In Kubernetes, the default RollingUpdate strategy replaces pods incrementally, but the default parameters are often too aggressive for production Node.js workloads.

Configuring safe rollout parameters

The two critical fields are maxUnavailable and maxSurge. For true zero-downtime, maxUnavailable must be 0, ensuring capacity never drops below the desired replica count during deployment.

spec:
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxUnavailable: 0
      maxSurge: 1
  template:
    spec:
      terminationGracePeriodSeconds: 30
      containers:
      - name: node-app
        readinessProbe:
          httpGet:
            path: /healthz/ready
            port: 3000
          initialDelaySeconds: 5
          periodSeconds: 5
          failureThreshold: 3
        livenessProbe:
          httpGet:
            path: /healthz/live
            port: 3000
          initialDelaySeconds: 10
          periodSeconds: 10

With maxSurge: 1, Kubernetes creates one new pod before terminating an old one. With maxUnavailable: 0, it will never terminate an old pod until the new one passes its readiness probe. This guarantees full capacity throughout the transition.

Why readiness probes matter more than liveness probes

A frequent cause of "zero-downtime" failures is confusing liveness and readiness. The liveness probe determines if a container should be restarted; the readiness probe determines if it should receive traffic. During deployment, Kubernetes adds the pod to the Service endpoints only after the readiness probe succeeds. If your readiness endpoint returns 200 before the application is fully initialized (database connected, caches warmed), traffic arrives prematurely and fails.

Your readiness check must verify actual dependency health, not just HTTP responsiveness. See my article on the four golden signals of monitoring for guidance on what constitutes a meaningful health signal versus a superficial heartbeat.

Time →Pod v1 ActivePod v2 Startingv2 Ready Probe Passv1 Drainingv1 TerminatedTraffic Flow During Transition● 100% → v1● v2 initializing (no traffic)● v2 receives traffic after readiness pass● v1 drains, then terminatesKey: maxUnavailable=0 ensures v1 stays active until v2 is confirmed readyNo moment exists where both pods are unavailable
Rolling update timeline for zero-downtime deployment for Node.js showing traffic handoff between pod versions

How do you compare deployment strategies for Node.js?

Rolling updates are the default, but they are not always the right choice. Understanding the trade-offs between strategies helps you match the approach to your application’s risk profile and architectural constraints.

StrategyDowntime RiskResource CostRollback SpeedBest For
Rolling UpdateLow (with correct probes)+1 pod during deploySlow (re-roll forward)Stateless APIs, microservices
Blue-GreenNear-zero2× full capacityInstant (switch router)Critical services, schema changes
CanaryVery low+10-20% capacityFast (shift traffic back)High-risk changes, ML models
RecreateFull downtimeNo overheadN/ADev/staging, non-critical batch

For most Node.js web applications, rolling updates with proper configuration provide the best balance. Blue-green becomes necessary when database schema changes are incompatible between versions, requiring two separate databases or careful migration sequencing. I cover these patterns in depth in blue-green and canary deploys on Kubernetes.

When rolling updates fail silently

Even with perfect configuration, rolling updates can produce brief error spikes if your load balancer has stale endpoint caches. In AWS EKS, the kube-proxy iptables rules may take several seconds to propagate after a pod becomes ready. During this window, some traffic may still route to terminating pods. Mitigations include:

  1. Setting preStop hooks with a 5-second sleep to allow endpoint propagation before SIGTERM
  2. Using Cilium or eBPF-based networking for faster endpoint updates (see Cilium eBPF networking for Kubernetes)
  3. Implementing client-side retry logic with exponential backoff for transient 502/503 responses

How do you handle database migrations during zero-downtime deploys?

Application code is only half the problem. Database schema changes are the most common source of deployment-related outages in Node.js systems. A rolling update assumes old and new code can coexist, which means your database schema must support both versions simultaneously.

The expand-contract pattern

Never rename or drop columns in a single deployment. Instead, use a three-phase approach:

  • Expand: Add the new column as nullable. Deploy new code that writes to both old and new columns but reads from the old column.
  • Migrate: Run a background job to backfill the new column for existing rows. Deploy code that reads from the new column with fallback to the old.
  • Contract: Remove the old column and the dual-write logic in a subsequent deployment.

This pattern ensures every intermediate state is valid for both running versions. For PostgreSQL-specific implementation details, including lock-safe DDL operations, refer to PostgreSQL administration essentials.

Connection pool management during restarts

Node.js applications typically use connection pools (pg-pool, mysql2/promise). During graceful shutdown, you must drain the pool before exiting. If you don’t, in-flight queries fail mid-execution when the process dies. The pool.end() method waits for active queries to complete and prevents new ones from being checked out. Pair this with the HTTP server drain described earlier to ensure the entire request chain completes cleanly.

Without Graceful ShutdownSIGTERM → Immediate process.exit()Active connections severedIn-flight requests return 502DB transactions rolled back abruptlyWebSocket clients disconnectedNo reconnection signal sentResult: User-visible errorsduring every deploymentWith Graceful ShutdownSIGTERM → server.close() calledNew connections rejected, existing drainedIn-flight requests complete normallyDB pool.end() waits for queriesWebSockets receive close frame 1001Clients reconnect to new instanceResult: Zero user-facing errorsDeployments are invisible
Side-by-side comparison of deployment behavior with and without graceful shutdown in Node.js

Conclusion

Zero-downtime deployment for Node.js is not a feature you enable—it is a contract you enforce across three layers. Your application must drain connections on SIGTERM. Your orchestrator must maintain capacity during transitions with correct probe configuration. Your database migrations must tolerate version coexistence. Missing any one of these breaks the chain and introduces the very downtime you are trying to eliminate.

Audit your current deployment against each layer described here. Start with the graceful shutdown implementation, as it is the most commonly missing piece and the fastest to fix. Then verify your Kubernetes rollout parameters and readiness probe logic. Finally, review your migration strategy for backward compatibility. If your team needs help designing or auditing a production-grade deployment pipeline, reach out to discuss your specific architecture.

Frequently Asked Questions

It is a release strategy ensuring application availability during updates by running new instances alongside old ones before switching traffic.

The cluster module forks worker processes that share server ports. During deployment, workers restart sequentially while others handle requests, preventing service interruption without external load balancers or complex orchestration tooling.

Yes, using pm2 reload with wait_ready and listen_timeout flags ensures new processes bind successfully before terminating old ones. This graceful reload prevents dropped connections during standard deployments on single servers in 2026.

Standard HTTP graceful shutdowns do not automatically migrate persistent sockets. You must implement connection draining logic to close existing WebSockets gracefully or use a message bus to reconnect clients to new instances seamlessly.

No. Tools like PM2, Docker Swarm, or Nginx upstreams handle rolling updates effectively. Kubernetes adds complexity best reserved for large-scale microservices requiring auto-scaling rather than simple zero-downtime releases for typical Node applications.

Use backward-compatible schema changes applied before code deployment. Never drop columns immediately; instead, add new fields, deploy updated code reading both, then remove old fields in a subsequent release cycle to prevent errors.

This occurs when old workers fail to release ports before new ones start. Configure SO_REUSEPORT in your server options or increase the kill timeout in your process manager to allow proper socket handover between generations.

Nginx acts as a reverse proxy buffering slow clients from Node workers. Using upstream blocks with multiple backends allows you to take specific nodes offline for updates while Nginx routes traffic only to healthy active instances.

Rolling updates replace instances incrementally, saving resources but risking version mixing. Blue-green deploys a full parallel environment first, enabling instant atomic switchovers and safer rollbacks at the cost of doubled infrastructure expenses.

Readiness probes verify the app accepts traffic before receiving requests. Liveness probes detect deadlocks triggering automatic restarts. Configuring these correctly in Docker or systemd ensures only fully initialized Node processes serve production user traffic.

Yes, running overlapping instances temporarily doubles memory consumption during the transition window. Ensure your server has sufficient headroom or configure process managers to limit concurrent spawning to avoid OOM kills during peak deployment phases.

Use Apache Bench or k6 to send continuous requests while running pm2 reload or docker compose up. Monitor response codes and latency spikes to verify no 502 errors occur and p99 latency remains stable throughout transitions.

SIGTERM signals the process to stop accepting new connections and finish pending requests. Implementing a handler for this signal allows cleanup of database pools and open files before exit, which is essential for zero-downtime behavior.

Running mixed versions briefly can expose inconsistent API behaviors or authentication states. Ensure feature flags gate incomplete functionality and validate that shared resources like Redis caches remain compatible across both old and new code versions.

Minimal extra cost exists using PM2 or Docker on existing VPS hardware. Cloud-native approaches using managed Kubernetes or AWS ECS Fargate increase monthly spend significantly due to required redundancy, monitoring overhead, and load balancer fees.