WebSocket Close Codes - Complete Reference
WebSocket close codes are numeric status codes sent inside close frames. They tell both sides of a connection why it ended. The codes range from 1000 to 4999 and are defined in RFC 6455. Understanding these codes is essential when you are building anything on top of the WebSocket protocol, because they drive your reconnection logic, error reporting, and debugging workflow.
How Close Frames Work
The WebSocket close process follows a specific handshake. Either side (client or server) can start it by sending a close frame. The other side then responds with its own close frame, and only after that exchange does the TCP connection actually shut down.
A close frame has a specific binary structure:
- Opcode:
0x8(indicates a close frame) - Payload (first 2 bytes): The close code as an unsigned 16-bit integer in network byte order (big-endian)
- Payload (remaining bytes): An optional UTF-8 encoded reason string, up to 123 bytes long
The 123-byte limit on the reason string comes from the maximum control frame payload size of 125 bytes, minus the 2 bytes used by the close code itself.
Here is what a typical close sequence looks like:
- The initiator sends a close frame with a code and optional reason.
- The recipient reads the close frame.
- The recipient sends back its own close frame (usually echoing the same code).
- The underlying TCP connection closes.
If the recipient does not respond with a close frame within a reasonable time, the initiator can drop the TCP connection directly. Browsers handle this timeout internally, and you have no control over it from JavaScript.
During the WebSocket handshake, both sides agree on the protocol. The close handshake is the mirror image of that process, providing an orderly shutdown rather than an abrupt TCP reset.
Standard Close Codes (1000-1015)
These codes are defined by the IANA WebSocket Close Code Number Registry. Some are sent in actual close frames, while others exist only as internal status indicators that your code will see in event handlers but that never appear on the wire.
| Code | Name | Description | Who Sends It |
|---|---|---|---|
| 1000 | Normal Closure | The connection fulfilled its purpose and is closing cleanly. This is the code you should send when everything went fine. | Either side |
| 1001 | Going Away | The endpoint is going away. A server might be shutting down, or a browser tab is navigating to a new page. | Either side |
| 1002 | Protocol Error | The endpoint received a frame that violates the WebSocket protocol. Malformed frames, incorrect opcodes, or other protocol-level problems trigger this. | Either side |
| 1003 | Unsupported Data | The endpoint received data it cannot accept. For example, a text-only endpoint received a binary frame, or vice versa. | Either side |
| 1004 | Reserved | Reserved for future use. You should not send this code. | Nobody |
| 1005 | No Status Received | A special code indicating that no close code was present in the close frame. This code is never actually sent in a frame. Your application sees it when the connection closed without a status code. WebSocket 1005 appears only in client-side APIs. | Never sent in frame |
| 1006 | Abnormal Closure | Another special code that is never sent in a frame. It indicates the connection was lost without a proper close handshake. You see this when the TCP connection drops unexpectedly, such as a network failure or a crash. WebSocket 1006 is one of the most common error codes you will encounter. | Never sent in frame |
| 1007 | Invalid Payload Data | The endpoint received a message with data inconsistent with the message type. A common case is a text message containing invalid UTF-8. | Either side |
| 1008 | Policy Violation | The endpoint is closing because it received a message that violates its policy. This is a generic code used when codes 1003 and 1009 do not apply, and you do not want to reveal specific details about the policy. | Either side |
| 1009 | Message Too Big | The endpoint received a message too large to process. Servers commonly set message size limits and return this code when a client exceeds them. | Either side |
| 1010 | Mandatory Extension | The client expected the server to negotiate one or more extensions (via the Sec-WebSocket-Extensions header), but the server did not include them in the handshake response. | Client only |
| 1011 | Internal Error | The server encountered an unexpected condition that prevented it from fulfilling the request. This was originally called “Internal Server Error” but was renamed in later revisions of the spec. | Server only |
| 1012 | Service Restart | The server is restarting. The client may reconnect, and if it does, it should use a randomized delay of 5-30 seconds. | Server only |
| 1013 | Try Again Later | The server is experiencing a temporary condition that forced it to reject the connection. The client may reconnect after some time. | Server only |
| 1014 | Bad Gateway | The server, acting as a gateway or proxy, received an invalid response from an upstream server. Similar to HTTP 502. | Server only |
| 1015 | TLS Handshake | Reserved to indicate that the connection was closed because the TLS handshake failed (for example, the server certificate could not be verified). Like 1005 and 1006, this code is never sent in a frame. | Never sent in frame |
Codes 1005, 1006, and 1015 are special. They are designated as codes that must not be set as a status code in a close frame by an endpoint. They exist only so that APIs (such as the browser’s CloseEvent) can represent specific failure conditions.
Private Use Codes (4000-4999)
The range 4000-4999 is reserved for application-level use. You can define any meaning you want for these codes in your own protocol. They are available for use by both client and server without registration.
Many popular services define custom close codes for their own protocols:
| Service / Library | Code | Meaning |
|---|---|---|
| Discord Gateway | 4000 | Unknown error |
| Discord Gateway | 4004 | Authentication failed |
| Discord Gateway | 4009 | Session timed out |
| Slack RTM | 4001 | Account inactive |
| Slack RTM | 4003 | Rate limited |
| Cloudflare Durable Objects | 4000 | Custom application error |
| Phoenix Channels | 4001 | Channel crashed on server |
When you design your own WebSocket-based protocol, define a clear set of custom codes in the 4000-4999 range. Document them alongside your API. A well-defined set of close codes makes debugging production issues far easier.
// Example: defining custom close codes for a chat application
const CloseCodes = {
KICKED_FROM_ROOM: 4001,
ROOM_DELETED: 4002,
SESSION_EXPIRED: 4003,
RATE_LIMITED: 4004,
MAINTENANCE: 4005,
};
// Server-side usage
ws.close(CloseCodes.SESSION_EXPIRED, "Your session has expired");
Reserved Ranges
The full code space is divided into four ranges:
| Range | Purpose | Registration Required |
|---|---|---|
| 0-999 | Unused. Not valid WebSocket close codes. | N/A |
| 1000-2999 | Protocol-level codes defined by the IANA registry. Codes in this range have specific meanings defined by the WebSocket spec or extensions. | Yes (IANA) |
| 3000-3999 | Codes for use by libraries, frameworks, and public APIs. If you are building a library others will use, register codes in this range. | First come, first served via IANA |
| 4000-4999 | Private use. Available for applications without any registration. | No |
Never send a code outside the range 1000-4999. Codes below 1000 are not valid, and the WebSocket protocol does not define any codes above 4999.
Handling Close Codes in Code
Browser JavaScript
When a WebSocket connection closes in the browser, the close event fires with a CloseEvent object. This object contains the code and reason properties.
const ws = new WebSocket("wss://example.com/socket");
ws.addEventListener("open", () => {
console.log("Connected");
});
ws.addEventListener("close", (event) => {
console.log(`Connection closed: code=${event.code}, reason="${event.reason}"`);
console.log(`Clean close: ${event.wasClean}`);
switch (event.code) {
case 1000:
console.log("Normal closure, no action needed");
break;
case 1001:
console.log("Server going away, reconnect after delay");
scheduleReconnect(5000);
break;
case 1006:
console.log("Connection lost unexpectedly, reconnect");
scheduleReconnect(1000);
break;
case 1008:
console.log("Policy violation, check your auth token");
refreshTokenAndReconnect();
break;
case 1013:
console.log("Server busy, try again later");
scheduleReconnect(30000);
break;
case 4003:
console.log("Custom: session expired, re-authenticate");
redirectToLogin();
break;
default:
console.log(`Unexpected close code: ${event.code}`);
scheduleReconnect(5000);
}
});
// Initiating a clean close from the client
function disconnect() {
ws.close(1000, "User requested disconnect");
}
The wasClean property tells you whether the close handshake completed properly. If wasClean is false, the connection dropped without a proper exchange of close frames.
Node.js with the ws Library
The ws library for Node.js gives you close codes in the close event callback.
import { WebSocketServer } from "ws";
const wss = new WebSocketServer({ port: 8080 });
wss.on("connection", (ws, req) => {
console.log(`New connection from ${req.socket.remoteAddress}`);
ws.on("message", (data) => {
// Process message
try {
const parsed = JSON.parse(data);
handleMessage(ws, parsed);
} catch (err) {
// Invalid JSON, close with 1003
ws.close(1003, "Expected JSON payload");
}
});
ws.on("close", (code, reason) => {
console.log(`Client disconnected: code=${code}, reason="${reason}"`);
});
ws.on("error", (err) => {
console.error("WebSocket error:", err.message);
// The close event will fire after this with code 1006
});
});
// Graceful shutdown
process.on("SIGTERM", () => {
console.log("Shutting down server...");
wss.clients.forEach((client) => {
client.close(1012, "Server is restarting");
});
wss.close(() => {
process.exit(0);
});
});
Notice how the server sends code 1012 (Service Restart) during a graceful shutdown. This tells clients they can reconnect after a short delay rather than treating the disconnection as a permanent failure.
Common Issues
Code 1006 Without Context
The most frustrating close code is 1006 (Abnormal Closure). WebSocket close 1006 errors appear whenever the connection drops without a close frame, which means the underlying cause is hidden from you. Common triggers include:
- Network connectivity loss (Wi-Fi drop, mobile network switch)
- The server process crashing before it can send a close frame
- A firewall or proxy terminating the connection
- Idle connection timeouts at the infrastructure level
To figure out why you are seeing 1006, check server-side logs first. The client side will never give you more information than “the connection went away.” Implementing a heartbeat/ping-pong mechanism helps you detect dead connections faster and distinguish between network issues and server problems.
Proxies and Load Balancers Closing Connections
Reverse proxies like Nginx, HAProxy, and cloud load balancers often have idle timeout settings. If no data flows over the WebSocket for a certain period, the proxy will terminate the connection. The client sees this as code 1006 since the proxy typically does not send a proper close frame.
To prevent this:
- Send periodic ping frames (the WebSocket protocol has native ping/pong support)
- Configure proxy timeouts to match your application needs
- In Nginx, set
proxy_read_timeoutto a value longer than your ping interval
location /ws {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 86400s; # 24 hours
proxy_send_timeout 86400s;
}
Browser Behavior Differences
Browsers handle certain edge cases differently:
- Close reason string: Some older browsers truncate the reason string or ignore it entirely. Do not rely on the reason string for programmatic logic. Use the close code instead.
- Code 1005 vs 1006: Some browsers report 1005 when the server closes the TCP connection without a close frame, while others report 1006. Always handle both in your reconnection logic.
- Navigating away: When a user closes a tab or navigates away, the browser sends code 1001 (Going Away). You cannot prevent this or delay it with JavaScript.
For more on securing your WebSocket connections against unexpected closures and attacks, see WebSocket Security best practices.
Reconnection Strategy by Close Code
Not all close codes should trigger a reconnection attempt. Some indicate permanent failures where retrying will never succeed.
| Code | Reconnect? | Strategy |
|---|---|---|
| 1000 | No | Connection closed normally. Only reconnect if the application logic requires a persistent connection. |
| 1001 | Yes | Server is going away. Reconnect with a 5-30 second randomized delay. |
| 1002 | No | Protocol error. Fix the client code before reconnecting. Retrying will produce the same error. |
| 1003 | No | The server does not accept your data type. Fix the client. |
| 1006 | Yes | Connection lost. Reconnect with exponential backoff starting at 1 second. |
| 1008 | Maybe | Policy violation. If this is an auth issue, refresh credentials first. Otherwise, do not retry. |
| 1009 | No | Your messages are too large. Reduce message size before reconnecting. |
| 1010 | No | Extension negotiation failed. Fix the client configuration. |
| 1011 | Yes | Server error. Reconnect with backoff. The server may have recovered. |
| 1012 | Yes | Server restarting. Reconnect with a 5-30 second randomized delay. |
| 1013 | Yes | Server overloaded. Reconnect with a longer delay (30-60 seconds). |
| 1014 | Yes | Upstream failure. Reconnect with backoff. |
| 4000-4999 | Depends | Application-defined. Consult the service documentation. |
A solid reconnection function applies exponential backoff with jitter to avoid the thundering herd problem, where all clients reconnect at the same time after a server restart:
function createReconnector(url, options = {}) {
const maxDelay = options.maxDelay || 30000;
const baseDelay = options.baseDelay || 1000;
let attempt = 0;
function connect() {
const ws = new WebSocket(url);
ws.addEventListener("open", () => {
attempt = 0; // Reset on successful connection
});
ws.addEventListener("close", (event) => {
if (!shouldReconnect(event.code)) {
console.log(`Close code ${event.code} is not recoverable. Giving up.`);
return;
}
attempt++;
const delay = Math.min(
baseDelay * Math.pow(2, attempt) + Math.random() * 1000,
maxDelay
);
console.log(`Reconnecting in ${Math.round(delay)}ms (attempt ${attempt})`);
setTimeout(connect, delay);
});
return ws;
}
function shouldReconnect(code) {
const noRetryCodes = [1000, 1002, 1003, 1009, 1010];
return !noRetryCodes.includes(code);
}
return connect();
}
// Usage
const ws = createReconnector("wss://example.com/socket", {
baseDelay: 1000,
maxDelay: 30000,
});
For a full walkthrough of building WebSocket connections in the browser, see the JavaScript WebSocket guide.
What to Read Next
- What is WebSocket? covers the protocol fundamentals, including frame types and how data flows between client and server.
- WebSocket Handshake explains the HTTP upgrade process that establishes every WebSocket connection.
- WebSocket Security walks through authentication patterns, origin checking, and protecting against common attacks.
- JavaScript WebSocket Guide provides a practical tutorial for building WebSocket clients in the browser with full code examples.