Every Redeploy Drops Every WebSocket — Designing Realtime Apps for It

·5 min read·Updated
websocketsrealtimedeploymentrestartsarchitecture

If your app holds WebSocket connections — a chat feature, a live dashboard, LiveView, Socket.IO, an SSE stream — every redeploy and every restart drops all of them at once. There's no connection draining and no overlap between the old container and the new one: your process gets a stop signal, roughly one second later it's killed, the container is removed, and a fresh one starts. Nothing waits for in-flight connections to finish. That's survivable and normal, but it has a consequence people miss: because every client disconnects at the same instant, they all try to reconnect at the same instant too. Reconnect jitter isn't a nicety here, it's the difference between a blip and a self-inflicted stampede.

What actually happens to the connection

A redeploy runs strictly in sequence:

  1. Your process receives a stop signal.
  2. About a second later, it's killed.
  3. The container is removed.
  4. The new image is pulled if it isn't already on the node.
  5. A new container is created and started.

Your sockets die at step 2 at the latest. A graceful-shutdown handler that tries to notify clients, flush state, and close cleanly will not finish — one second is not enough for anything ambitious.

This also means configuration changes are deploys. Changing an environment variable, a port, a volume, or a CPU/RAM preset recreates the container, so each one costs you every open connection. Batch them rather than making five changes across an afternoon.

The reconnect stampede

Here's the part that distinguishes this from ordinary "connections drop sometimes" advice.

Under normal conditions, clients disconnect independently — someone closes a laptop, a phone loses signal. Reconnects arrive spread out. A redeploy is different: every client disconnects in the same millisecond. If they all retry after a fixed one-second delay, your fresh container's first act is to accept its entire user base simultaneously, do all the authentication and session setup at once, and possibly fall over — after which they all retry together again.

The fix is standard and frequently omitted from generated code: exponential backoff with jitter.

let attempt = 0;

function reconnect() {
  const base = Math.min(1000 * 2 ** attempt, 30000);
  const delay = base * (0.5 + Math.random());   // 50-150% of base
  attempt++;
  setTimeout(connect, delay);
}

The randomised multiplier is what spreads the herd. Without it, backoff alone keeps everyone synchronised — they just stampede on a slower schedule. Reset attempt to zero once a connection has been stable for a few seconds, not the moment it opens, or a flapping connection climbs the backoff forever.

Since a redeploy also involves pulling an image, a smaller image shortens the window before clients can succeed.

Don't keep state only in the socket

A dropped connection destroys whatever lived in it. If your server holds "which room this user is in" or "how far through this stream they are" purely in per-connection memory, a redeploy loses it and the client comes back to a server with amnesia.

Two habits fix this:

  • Key state by user or session, not by connection. The socket is a transport; the state belongs somewhere that outlives it — memory keyed by user ID at minimum, a volume or database if it must survive the restart too.
  • Let the client tell you where it was. A last-seen message ID on reconnect lets you replay the gap. This is exactly the pattern MCP's Streamable HTTP transport uses with Last-Event-ID, and it's worth copying — more on that.

Getting this right turns a redeploy from "users notice" into "users don't".

Heartbeats and idle connections

Your route is TLS termination and hostname routing — no per-app timeout or buffering middleware is configured. But intermediaries between your users and you (corporate proxies, mobile carriers, home routers) will happily drop a connection that goes quiet.

So send application-level pings on an interval and treat a missed pong as a dead connection rather than waiting for TCP to notice. Most WebSocket libraries have this built in and turned off by default. It's also how you detect half-open connections, which otherwise linger and consume memory for nothing.

One container, and what follows from it

An app runs as a single container — there's no replica count and no horizontal scaling. Two consequences, one good and one to plan around:

Good: no sticky sessions to configure, no shared session store required, no cross-instance message bus. Every connection lands on the same process, so in-memory pub/sub for a chat room genuinely works. That's a real simplification versus a multi-instance setup.

To plan around: every connection shares one process's memory and one CPU time budget. Idle sockets are cheap but not free, and the cost that surprises people is broadcast fan-out — sending one message to N connections is N serialisations and N writes. A room with a few hundred members chatting actively is a different workload from a few hundred idle dashboards.

Since there's no health probing, add your own liveness check: a process that has stopped accepting connections but is still running looks perfectly healthy from outside.

What you can't do here

Being clear about the limits, because the workarounds elsewhere don't apply:

  • No blue/green or rolling deploys. The old container is gone before the new one exists. Deploys have a gap.
  • No connection draining. You can't ask for a grace period; one second is what you get.
  • No load balancer to hold connections while you swap the backend.

So the practical advice is unglamorous: deploy when your users are least active, batch configuration changes into one restart, and make the client-side reconnect good enough that the gap is invisible. For most realtime side projects and small apps, that's genuinely sufficient.

FAQ

Do WebSocket connections survive a redeploy?

No. The container is stopped and replaced with no overlap, so every connection drops and clients must reconnect.

How long do I have to close connections gracefully?

About a second between the stop signal and the kill. Long shutdown handlers won't complete.

Why do all my clients reconnect at once?

They all disconnected at once. Use exponential backoff with random jitter so retries spread out instead of arriving together.

Do I need sticky sessions?

No. An app is a single container, so every connection reaches the same process — which also means in-memory pub/sub works.

Does changing an environment variable drop connections?

Yes. Configuration changes recreate the container, so batch them into one restart.

How many WebSocket connections can I hold?

More than you'd expect if they're idle — memory per connection is small. Broadcast-heavy workloads hit CPU well before connection counts become the limit.

Wrapping up

Realtime apps are a good fit for a small container right up until you assume the process is permanent. It isn't: every deploy and every configuration change replaces it, with about a second's notice and nothing to hold connections in the meantime. Design the client to expect that — backoff with jitter, a resume point, heartbeats — and design the server to keep state somewhere the socket doesn't own. Then a deploy is a flicker instead of an outage, and you can ship in the middle of the day without thinking about it.