8 min
  • WebSockets
  • TCP/IP

Keeping a WebSocket alive

An open socket is not a working one. TCP will happily let a program write to a peer that vanished minutes ago, and nothing reports the problem until something forces the issue.

Two things go wrong on a long-lived connection, and both are invisible:

  • The peer disappears — crashed, rebooted, driven into a tunnel — and your end still believes the connection is fine.
  • A middlebox on the path decides the flow is idle and drops its state. A NAT table or a load balancer commonly gives an idle connection 30 to 60 seconds.
Loading the WebSocket Viewer simulation…

Eight bytes, twice a minute

A ping frame costs eight bytes and buys both fixes at once: proof the peer is alive, and enough traffic that nothing on the path considers the flow idle. The interval must beat the shortest timeout anywhere on the path, which is why 25 seconds is a common choice.

Two details that surprise people:

  • Answering a ping is an obligation, not a courtesy, and browsers do it automatically without telling the page. The pong must echo the ping's payload back byte for byte, which is what lets a sender match a pong to its ping and measure a round trip.
  • Browsers cannot send pings. The WebSocket API has no method for it. The heartbeat is therefore the server's job in any browser-facing deployment.
Check yourself
You are writing a browser client for a WebSocket API. How do you implement the heartbeat?

Closing properly, and the code that is never sent

A clean close is a handshake: one Close frame out, one echoed back, and only then does TCP come down. wasClean is true only when both crossed — which is the one reliable way to distinguish an intentional shutdown from a connection that simply died.

Loading the WebSocket Viewer simulation…

Four close codes — 1004, 1005, 1006 and 1015 — can never appear in a frame at all. They are what a local API reports when there is no frame to read a code from. 1006 in particular means "no Close frame arrived", and it carries no reason because there was nothing to carry one.

That is exactly what you get when the transport vanishes:

Loading the WebSocket Viewer simulation…
Predict what happens next
Ten thousand clients lose their connection when a server restarts. Each retries after exactly one second. What happens?

A reconnection is a genuinely new connection: new handshake, new key, and no memory of the old one. Any messages sent during the gap are gone unless your application asks for them back — which is the one thing SSE gets for free.

Key takeaways

  • An open socket is not a working one: writes to a vanished peer keep succeeding for minutes.
  • A ping costs eight bytes and buys both liveness proof and enough traffic to survive NAT and load-balancer idle timeouts.
  • Browsers answer pings automatically and cannot send them, so the heartbeat is the server’s job.
  • A clean close is a handshake in both directions; 1006 means no Close frame ever arrived and carries no reason.
  • Exponential backoff without jitter keeps a disconnected herd synchronised — the randomisation is what protects the server.

Take it further