The Standard Close Code Ranges
Every WebSocket close frame can carry a 2-byte unsigned integer status code, and the specification reserves specific ranges with specific meanings: 1000-2999 are reserved for the protocol itself (1000 is normal closure, 1001 going away, 1002 protocol error, 1003 unsupported data, 1007 invalid payload data, 1008 policy violation, 1009 message too big, 1011 internal server error), 3000-3999 are reserved for use by libraries, frameworks, and registered extensions, and 4000-4999 are left entirely open for private application use between a client and server that both understand the same custom scheme. Codes 1004, 1005, 1006, and 1015 are reserved but must never actually appear on the wire — they exist only as internal signal values a local implementation reports when it detects specific failure conditions.
Cricket analogy: It's like the ICC's playing conditions rulebook reserving specific clause numbers for universally recognized dismissals (bowled, caught, LBW) while leaving other numbers open for a domestic board's own local match regulations that only apply within that competition.
Choosing the Right Close Code
Picking an accurate close code matters because client code, monitoring dashboards, and reconnect logic all branch on it. A server should send 1000 with a clear reason string only for genuinely intentional, successful completions, like a game session ending normally; it should use 1008 (policy violation) when rejecting a client for something like a failed auth check or rate-limit breach, 1011 when an unexpected server-side exception occurred that the client shouldn't necessarily retry immediately, and 1003 or 1007 when the client sent data the server structurally cannot process, such as binary data on an endpoint that only accepts JSON text or a text frame with invalid UTF-8. Overusing the generic 1000 for every case, including actual failures, is a common mistake that makes debugging production issues much harder because client-side logs lose the distinction between 'this ended on purpose' and 'this ended because something broke.'
Cricket analogy: It's like a scorer correctly recording the exact mode of dismissal — bowled, run out, or retired hurt — rather than lumping every wicket under a generic 'out,' because the specific reason matters enormously for later analysis of a bowler's or batter's record.
// Client-side reconnect logic that respects the close code
function connect(url) {
const socket = new WebSocket(url);
socket.addEventListener('close', (event) => {
console.log(`Closed: code=${event.code} reason=${event.reason}`);
if (event.code === 1000 || event.code === 1001) {
// Normal closure or server going away intentionally: don't auto-reconnect
return;
}
if (event.code === 1008 || event.code === 4001) {
// Policy violation / custom auth failure: don't retry, surface to the user
showAuthError();
return;
}
// Everything else (1006, 1011, network drop) is worth retrying with backoff
scheduleReconnect(url);
});
return socket;
}
let attempt = 0;
function scheduleReconnect(url) {
const delay = Math.min(30000, 1000 * 2 ** attempt++);
setTimeout(() => connect(url), delay);
}Building Resilient Reconnect Logic
Robust WebSocket clients treat disconnection as a normal, expected event rather than an exception, and implement exponential backoff with jitter for reconnect attempts rather than retrying instantly in a tight loop, which can hammer a struggling server right when it can least handle the load or trigger a client-side ban from a rate limiter. Beyond just reopening the socket, resilient logic needs to resynchronize application state after a gap — for example, requesting any messages missed since the last known sequence number or timestamp — because a reconnect only re-establishes the transport; it does nothing to recover data that would have arrived during the outage unless the application layer explicitly accounts for it.
Cricket analogy: It's like a team returning to the field after a rain delay not simply resuming as if no time passed, but the umpires recalculating a revised target using the Duckworth-Lewis-Stern method to account for what was actually lost during the interruption.
Exponential backoff with jitter typically looks like delay = min(maxDelay, baseDelay * 2^attempt) plus a small random offset, which prevents a large fleet of clients from all retrying in synchronized bursts (a 'thundering herd') after a shared outage, such as a load balancer restart.
- Close codes 1000-2999 are protocol-reserved, 3000-3999 for libraries/extensions, and 4000-4999 are free for private application use.
- Codes 1004, 1005, 1006, and 1015 are reserved but must never actually appear on the wire; they're local-only signals.
- Use 1000 only for genuinely successful, intentional closures — overusing it for failures hides real problems.
- 1008 (policy violation) suits auth/rate-limit rejections; 1011 suits unexpected server errors; 1003/1007 suit malformed data.
- Resilient clients treat disconnection as normal and use exponential backoff with jitter rather than tight-loop retries.
- Reconnecting only restores the transport — applications must explicitly resynchronize any state or messages missed during the gap.
- Branching reconnect behavior on the close code (e.g., not retrying after 1008) avoids wasted retries against permanent failures.
Practice what you learned
1. Which close code range is reserved entirely for private application use between a client and server that agree on their own scheme?
2. Why does close code 1006 never actually appear in a close frame on the wire?
3. Which close code is most appropriate for a server rejecting a connection due to a failed authentication check?
4. Why should reconnect logic use exponential backoff with jitter instead of retrying immediately in a loop?
5. What does a successful WebSocket reconnect restore by itself, without additional application logic?
Was this page helpful?
You May Also Like
Ping/Pong and Heartbeats
How WebSocket ping and pong control frames keep connections alive and let both sides detect dead peers.
Opening and Closing Connections
How a WebSocket connection is established through the HTTP upgrade handshake and gracefully terminated using the closing handshake.
Subprotocols Explained
How the Sec-WebSocket-Protocol header lets client and server agree on an application-level messaging convention over a raw WebSocket connection.
Related Reading
Related Study Notes in Web Development
Browse all study notesWebAssembly Study Notes
WebAssembly · 30 topics
Web DevelopmentgRPC Study Notes
Protocol Buffers · 30 topics
Web DevelopmentSpring Boot Study Notes
Java · 30 topics
Web DevelopmentFlask Study Notes
Python · 30 topics
Web DevelopmentDjango Study Notes
Python · 30 topics
Web DevelopmentNext.js Study Notes
JavaScript · 30 topics