Skip to content

Close Codes & Resume

We have covered the accidental end of a connection — drops, detection, reconnect. This final lesson covers the deliberate end: the clean close handshake, the numeric codes that explain why a connection ended, and the payoff of doing reconnection well — catching up on everything you missed while you were gone.

A clean WebSocket close is a small two-step conversation, not a yank of the cable:

  1. One side sends a close frame carrying a numeric code and an optional short reason string.
  2. The other side replies with its own close frame.
  3. Only then is the underlying TCP connection torn down.

This is why the foundations module insisted a WebSocket close is symmetric. When it works, both ends agree the connection is over and nobody is left guessing. The browser surfaces this through the onclose event, whose event carries code, reason, and a wasClean flag telling you whether the full handshake completed.

sequenceDiagram
  participant A as Side A
  participant B as Side B
  A->>B: close frame (code, reason)
  B-->>A: close frame (echo)
  Note over A,B: TCP connection torn down
  Note over A,B: onclose fires with wasClean = true
The clean close handshake

The code is the connection’s cause of death. Knowing them tells you whether to reconnect, give up, or fix a bug. The ones that matter:

  • 1000 — Normal Closure. Everything finished as intended. The deliberate ws.close() your app calls. Do not auto-reconnect on this; the close was on purpose.
  • 1001 — Going Away. A side is leaving — a browser tab navigating away, or a server shutting down for deploy. A client seeing 1001 should usually reconnect after a moment; the server is coming back.
  • 1006 — Abnormal Closure. The one you met in the drop lessons. It means the connection died without a proper close frame — and critically, no code is ever sent over the wire for 1006; the browser fabricates it locally to tell you “this ended abnormally”. Every accidental drop surfaces as 1006. This is your signal to reconnect with backoff.
  • 1011 — Internal Error. The server hit an unexpected condition and is ending the connection. Reconnect is reasonable, but persistent 1011s mean a server bug to chase.
  • 4000-4999 — Application codes. This whole range is reserved for you. The protocol will never define them, so they are yours to assign meaning: 4001 might be “auth token expired”, 4002 “kicked by an admin”, 4008 “you are rate limited”. A client can branch on these — refresh the token and reconnect on 4001, but stop and show a message on 4002.

The practical rule: 1000 means stop, most others mean reconnect, and 4000+ means do whatever you decided that code means. Always include a code (and ideally a reason) when you close, so the other side can make exactly that decision.

When your app deliberately ends a connection — the user logs out, navigates away, or closes a feature — close it properly:

function shutdown(ws: WebSocket) {
// 1000 = normal. The peer learns this was intentional and will not reconnect.
ws.close(1000, 'client navigating away');
}

The reason a clean close matters is the flip side of everything in this module: a connection that ends with code 1000 tells the other end “do not reconnect, this was meant to happen”. Skip it and you leave the server holding a phantom connection it must heartbeat to death — exactly the half-open mess we worked to avoid.

Here is the reward for all the reconnect machinery. When a client drops and comes back, a naive app has a hole: every message the server sent during the gap is lost. The cure is resumable state keyed by a last-seen id.

The pattern:

  1. The server stamps every message with a monotonically increasing id: 1, 2, 3, ....
  2. The client remembers the id of the last message it successfully processed — its lastSeenId.
  3. On reconnect, the client sends lastSeenId to the server as part of a resume request.
  4. The server replays every message with an id greater than lastSeenId, then resumes live delivery.

The client comes back exactly where it left off, with no gap and no duplicates. (The server needs a short buffer of recent messages to replay from; truly old gaps may require a full state refresh instead, but for brief drops, replay is seamless.)

The demo below uses an in-page socket with the real WebSocket API. The mock server numbers every message it sends. We let the client receive a few, then the connection drops (close code 1006) while the server keeps producing messages. On reconnect, the client sends its lastSeenId and the server replays exactly the messages it missed — then resumes live.

JavaScript

Watch the order: the client receives messages 1 and 2 live, then the connection drops (code 1006) while message 3 is still queued on the server. The client reconnects, sends lastSeenId=2, and the server replays only message 3 — no gap, no duplicate of 1 or 2. That seamless catch-up is what every well-built real-time app does behind the scenes, and it is only possible because reconnection and a last-seen id work together.

What does close code 1006 (Abnormal Closure) signify?
After which close code should a client generally NOT auto-reconnect?
How does a last-seen id let a reconnecting client resume without gaps or duplicates?