Zero-Downtime Deployment for Bun

Khimananda Oli 9 min read Programming and Languages
Zero-Downtime Deployment for Bun

By Khimananda Oli | Last reviewed: August 2026

Achieving zero-downtime deployment for Bun requires more than just restarting the process; without specific OS-level integration, active requests will drop during every release. While Bun is incredibly fast, its single-binary nature means standard restarts sever existing TCP connections instantly. To solve this, you must decouple socket ownership from the application process using systemd socket activation or a load-balanced rolling update strategy. This guide covers the exact configuration needed to make your Bun deployments truly seamless on Linux servers.

How does systemd socket activation enable zero-downtime deployment for Bun?

Systemd socket activation is the most robust method for achieving zero-downtime deployment for Bun on a single server. In a traditional setup, when you issue systemctl restart bun-app, systemd sends a SIGTERM to the old process, which closes the listening socket before the new process can bind to it. Even with graceful shutdown logic, there is a race condition where new connections are refused because no process is holding the port.

Socket activation solves this by having systemd itself hold the listening socket. When traffic arrives, systemd passes the open file descriptor to the Bun process. During a restart, systemd keeps the socket open and accepting connections in a kernel backlog while the new Bun instance starts up. Once the new process signals readiness, systemd hands off the queued connections. This eliminates the "connection refused" window entirely.

Client RequestsystemdHolds Socket :3000Buffers ConnectionsPass FDBun ProcessInherits SocketSocket remains open during restart — zero dropped packets
Systemd socket activation maintains the listening port independently of the Bun process lifecycle.

Configuring the socket unit

Create a dedicated socket unit file at /etc/systemd/system/bun-app.socket. This unit defines the port and buffering behavior independent of your application code.

[Unit]
Description=Bun App Socket

[Socket]
ListenStream=0.0.0.0:3000
Accept=no
TriggerLimitIntervalSec=10s
TriggerLimitBurst=200

[Install]
WantedBy=sockets.target

The Accept=no directive is critical. It tells systemd to pass the listening socket itself rather than spawning a new process per connection. This matches how Bun expects to manage its own HTTP server internally. The trigger limits prevent connection storms during recovery scenarios.

Adapting the service unit for socket inheritance

Your service unit at /etc/systemd/system/bun-app.service must reference the socket and avoid binding to a hardcoded port in the application code. Bun automatically detects the inherited file descriptor when started via systemd.

[Unit]
Description=Bun Application Service
Requires=bun-app.socket
After=bun-app.socket

[Service]
ExecStart=/opt/bun/bin/bun run /var/www/app/server.ts
Restart=on-failure
RestartSec=5
User=www-data
Group=www-data
Environment=NODE_ENV=production
NotifyAccess=all
Type=notify

[Install]
WantedBy=multi-user.target

Note the Type=notify setting. For true zero-downtime behavior, your Bun application should call sd_notify("READY=1") once initialization completes. If your app doesn't support sd_notify, use Type=simple but accept that systemd may forward traffic slightly before the app is fully ready. For production workloads handling sensitive transactions, implementing the notification protocol is non-negotiable.

How do you configure Nginx as a load balancer for Bun rolling updates?

While socket activation handles single-instance deployments, teams running high-traffic applications often prefer a rolling update strategy behind Nginx. This approach aligns with patterns discussed in blue-green and canary deployment strategies, adapted for bare-metal or VPS environments. The core principle is maintaining at least one healthy upstream while others recycle.

Nginx acts as the buffer between clients and your Bun instances. By configuring multiple upstream servers on different ports (or Unix sockets), you can restart Bun processes sequentially without ever removing all backends from the pool. This is particularly valuable when deploying database migrations alongside code changes, as covered in zero-downtime migration guides.

ClientsNginx LBHealth ChecksRetry LogicBufferingBun :3001Bun :3002Bun :3003 (draining)Rolling restart: update one instance at a time while others serve traffic
Nginx distributes traffic across multiple Bun instances enabling safe sequential restarts.

Nginx upstream configuration with health checks

Configure your Nginx upstream block to include retry logic and failure detection. This ensures that if a Bun instance fails mid-request during deployment, Nginx transparently retries on another backend.

upstream bun_cluster {
    server unix:/run/bun/app-1.sock max_fails=2 fail_timeout=10s;
    server unix:/run/bun/app-2.sock max_fails=2 fail_timeout=10s;
    server unix:/run/bun/app-3.sock max_fails=2 fail_timeout=10s backup;
    
    keepalive 32;
}

server {
    listen 80;
    server_name api.example.com;

    location / {
        proxy_pass http://bun_cluster;
        proxy_next_upstream error timeout http_502 http_503;
        proxy_next_upstream_tries 2;
        proxy_connect_timeout 5s;
        
        # Critical for WebSocket support in Bun
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}

Using Unix sockets instead of TCP ports reduces overhead and avoids port exhaustion during rapid restarts. The keepalive directive maintains persistent connections to Bun, reducing handshake latency significantly under load.

What are the common pitfalls when implementing graceful shutdown in Bun?

Even with perfect infrastructure, your application code can sabotage zero-downtime deployment for Bun if it doesn't handle termination signals correctly. A common mistake is assuming Bun's default signal handling is sufficient for production. While Bun does trap SIGTERM, complex applications with database pools, message queue consumers, or long-running SSE streams need explicit cleanup logic.

Another frequent issue is mismatched timeouts. If your Nginx proxy_read_timeout is 60 seconds but your systemd TimeoutStopSec is 30 seconds, systemd will SIGKILL the process while Nginx still expects it to be draining connections. Always ensure your infrastructure timeouts form a coherent chain: app drain time < systemd stop timeout < proxy timeout. Observability plays a key role here; refer to the four golden signals to validate that your deployment pipeline isn't silently dropping requests.

Implementing proper signal handling

Add explicit shutdown hooks to your Bun server entry point. This ensures in-flight requests complete and resources release cleanly before the process exits.

const server = Bun.serve({
  port: process.env.PORT || 3000,
  fetch(req) {
    return new Response("Hello World");
  },
});

const shutdown = async (signal: string) => {
  console.log(`Received ${signal}. Starting graceful shutdown...`);
  
  // Stop accepting new connections
  server.stop(true); // true = wait for in-flight requests
  
  // Close database pools, Redis clients, etc.
  await db.end();
  await redis.quit();
  
  console.log("Graceful shutdown complete.");
  process.exit(0);
};

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

The server.stop(true) call is essential. Passing true tells Bun to wait for existing requests to finish rather than aborting them immediately. Without this parameter, even socket activation won't save your users from partial responses.

How do socket activation and rolling updates compare for Bun deployments?

Choosing between systemd socket activation and Nginx-based rolling updates depends on your operational complexity tolerance and traffic profile. Both achieve zero-downtime deployment for Bun, but they optimize for different constraints.

CriteriaSystemd Socket ActivationNginx Rolling Updates
ComplexityLow (single unit config)Medium (multiple units + LB)
Resource OverheadMinimal (one process)Higher (N+1 processes)
Deployment SpeedInstant handoffSequential drain cycles
Fault IsolationNone (single point)High (independent instances)
Best ForAPIs, internal tools, low-memory VPSHigh-traffic public apps, critical SLAs
Horizontal ScalingRequires external LB anywayBuilt-in distribution

For most Nepal-based startups and SMEs operating on budget VPS instances, socket activation provides the best balance of reliability and resource efficiency. Reserve rolling updates for applications where a single failed deployment would cause significant business impact or where you're already running multiple instances for performance reasons.

Start: Deploy Bun AppMultiple instances needed?(HA / High Traffic / Blue-Green)NoYesSocket ActivationSingle unit, minimal RAMNginx Rolling UpdateMulti-port, fault isolatedAdd graceful shutdown hooksConfigure upstream health checks
Decision matrix for selecting the appropriate zero-downtime strategy based on scale and requirements.

How do you verify zero-downtime behavior during Bun deployments?

Configuration alone doesn't guarantee success; you must validate that your deployment actually drops zero connections. Many engineers configure socket activation or rolling updates and assume it works until a customer reports an error weeks later. Verification should be part of your CI/CD pipeline, not an afterthought.

Use a continuous request generator like hey or wrk during deployment. Run the load test in a separate terminal while triggering your deployment script. Any non-2xx response or connection reset indicates a gap in your zero-downtime setup. For production validation, integrate deployment metrics into your observability stack as described in Prometheus monitoring fundamentals. Track bun_requests_total with status code labels and alert on any spike in 502/503 errors correlated with deployment timestamps.

Automated verification script

Incorporate this check into your deployment automation. The script below hammers the endpoint throughout the deploy and fails the pipeline if any request drops.

#!/bin/bash
# Run in background during deploy
hey -n 10000 -c 50 -q 100 https://api.example.com/health > /tmp/loadtest.log &
LOAD_PID=$!

# Trigger deployment
systemctl reload-or-restart bun-app

# Wait for load test to complete
wait $LOAD_PID

# Check for failures
FAILURES=$(grep -c "Error\|502\|503\|connection reset" /tmp/loadtest.log)
if [ "$FAILURES" -gt 0 ]; then
  echo "DEPLOYMENT FAILED: $FAILURES dropped requests detected"
  exit 1
fi

echo "Zero-downtime verified successfully"

Next Steps for Production-Ready Bun Deployments

Implementing zero-downtime deployment for Bun transforms your release process from a source of anxiety into a routine operation. Start with systemd socket activation for single-instance deployments—it's simpler, uses fewer resources, and covers the majority of use cases for teams in Nepal and globally. Graduate to Nginx rolling updates only when your traffic volume or availability requirements demand horizontal scaling. Regardless of the method chosen, always pair infrastructure changes with proper application-level signal handling and automated verification. If you need help auditing your current Bun deployment pipeline or designing a compliant infrastructure for SOC 2 or ISO 27001, reach out to discuss your architecture.

Frequently Asked Questions

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

Yes, Bun handles SIGTERM natively to stop accepting connections while finishing active requests before exiting the process.

Use Type=notify with ExecStartPre to spawn the new process and ExecStopPost to verify health before killing the old service unit.

No, you need a reverse proxy like Caddy or Nginx to buffer traffic between old and new Bun sockets during the transition window.

Bind new Bun servers to unique temporary Unix sockets then atomically rename them to the production path to avoid port conflicts.

Cluster mode spawns multiple workers so you can restart them individually using round-robin signaling to maintain total request throughput capacity.

WebSockets are stateful and do not transfer between processes requiring sticky sessions or a pubsub backend to reconnect clients gracefully.

Bun uses significantly less memory allowing smaller instance sizes which reduces cloud infrastructure costs for identical high-availability deployment architectures.

Run backward-compatible migrations before deploying code to ensure both old and new Bun instances can query the schema safely.

Expose a lightweight /health route returning 200 OK that verifies database connectivity without executing heavy business logic or external API calls.

Yes, orchestrators like Kubernetes manage rolling updates automatically but require proper liveness probes configured for Bun startup times.

Set timeouts slightly longer than your p99 request latency typically thirty seconds to prevent dropping slow in-flight user requests.

Yes, use pm2 reload with the --update-env flag to gracefully cycle Bun clusters without interrupting active TCP connections.

The previous process has not fully released the port so implement SO_REUSEPORT or wait for the old PID to exit completely.

Monitor error rates and response latency metrics in Grafana during the deploy window to confirm no requests returned 502 or 504 status codes.