Skip to content

Detecting Dead Connections

The heartbeat from the last lesson sends a pulse. But a pulse you send is worthless unless you check the answer comes back. This lesson is about that check: how to turn “the pong did not arrive” into a confident decision to declare the connection dead and tear it down.

Sending is half; expecting is the other half

Section titled “Sending is half; expecting is the other half”

A common, broken first attempt is to send a ping on a timer and stop there. That detects nothing. The whole detection happens in the gap between the ping you sent and the pong you expected. The logic is:

  1. Send a ping. Note that a pong is now owed to you.
  2. Start a deadline — say, the next interval or a few seconds.
  3. If the matching pong arrives before the deadline, good: clear the debt, the connection is alive.
  4. If the deadline passes with the debt still outstanding, you have a missed pong. Treat the connection as dead.

The single most useful variable in your client is therefore something like pongOutstanding (or a count of unanswered pings). When it crosses a threshold, you act. Many production clients tolerate one or two misses before declaring death — a single missed pong might be a momentary hiccup; three in a row is a corpse.

Half-open connections — the reason this is hard

Section titled “Half-open connections — the reason this is hard”

The villain behind all this is the half-open connection. A connection is half-open when one side believes it is still connected while the other has silently vanished. Picture a phone call where the other person’s phone died mid-sentence: you are still holding the receiver, talking, hearing nothing, with no dial tone to tell you the call is over.

This happens constantly in real networks:

  • A laptop lid closes; the OS freezes the socket without a clean close.
  • A mobile device switches from Wi-Fi to cellular, abandoning the old socket.
  • A NAT or load balancer silently evicts an idle connection from its table.

In every case, no close event fires on the surviving side, because no FIN packet ever reached it. Your WebSocket still says OPEN. The only way you will ever learn the truth is by sending something and noticing nothing came back — which is exactly what the heartbeat-plus-deadline gives you.

Once you have decided the connection is dead, do not be polite about it. A clean close() tries to perform a handshake with a peer that is already gone — there is nobody to handshake with, so it may hang. Instead, terminate: abandon the dead socket immediately and move straight to reconnecting. Browsers expose close(); Node’s ws library adds terminate() for exactly this brutal case. The decision flow:

stateDiagram-v2
  [*] --> Healthy
  Healthy --> Waiting: send ping, pong now owed
  Waiting --> Healthy: pong arrived in time
  Waiting --> Suspect: deadline passed, pong missed
  Suspect --> Healthy: a later pong arrived
  Suspect --> Dead: misses exceeded threshold
  Dead --> Terminate: abandon socket, do not handshake
  Terminate --> [*]: hand off to reconnect
Deciding a connection is dead

The demo below uses an in-page socket with the real WebSocket API. This mock answers the first couple of pings normally — then it goes silent, simulating a connection that has died without firing onclose (a half-open connection). The client runs a heartbeat with a deadline and flags the connection dead the moment a pong is overdue.

JavaScript

You will see two healthy ping/pong pairs, then the mock falls silent. The client keeps sending, the unanswered-pong count climbs, and once it crosses the threshold the client declares the connection dead and terminates it — without ever waiting for an onclose that was never going to come. That is detection: you stopped trusting silence and acted on the missing answer.

What actually detects a dead connection?
What is a half-open connection?
Why terminate instead of cleanly close a connection you have declared dead?