tests.ws

WebSocket Close Codes - Complete Reference

websocket close codes reference rfc 6455 error handling

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:

  1. The initiator sends a close frame with a code and optional reason.
  2. The recipient reads the close frame.
  3. The recipient sends back its own close frame (usually echoing the same code).
  4. 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.

CodeNameDescriptionWho Sends It
1000Normal ClosureThe connection fulfilled its purpose and is closing cleanly. This is the code you should send when everything went fine.Either side
1001Going AwayThe endpoint is going away. A server might be shutting down, or a browser tab is navigating to a new page.Either side
1002Protocol ErrorThe endpoint received a frame that violates the WebSocket protocol. Malformed frames, incorrect opcodes, or other protocol-level problems trigger this.Either side
1003Unsupported DataThe endpoint received data it cannot accept. For example, a text-only endpoint received a binary frame, or vice versa.Either side
1004ReservedReserved for future use. You should not send this code.Nobody
1005No Status ReceivedA 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
1006Abnormal ClosureAnother 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
1007Invalid Payload DataThe endpoint received a message with data inconsistent with the message type. A common case is a text message containing invalid UTF-8.Either side
1008Policy ViolationThe 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
1009Message Too BigThe 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
1010Mandatory ExtensionThe 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
1011Internal ErrorThe 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
1012Service RestartThe server is restarting. The client may reconnect, and if it does, it should use a randomized delay of 5-30 seconds.Server only
1013Try Again LaterThe server is experiencing a temporary condition that forced it to reject the connection. The client may reconnect after some time.Server only
1014Bad GatewayThe server, acting as a gateway or proxy, received an invalid response from an upstream server. Similar to HTTP 502.Server only
1015TLS HandshakeReserved 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 / LibraryCodeMeaning
Discord Gateway4000Unknown error
Discord Gateway4004Authentication failed
Discord Gateway4009Session timed out
Slack RTM4001Account inactive
Slack RTM4003Rate limited
Cloudflare Durable Objects4000Custom application error
Phoenix Channels4001Channel 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:

RangePurposeRegistration Required
0-999Unused. Not valid WebSocket close codes.N/A
1000-2999Protocol-level codes defined by the IANA registry. Codes in this range have specific meanings defined by the WebSocket spec or extensions.Yes (IANA)
3000-3999Codes 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-4999Private 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_timeout to 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.

CodeReconnect?Strategy
1000NoConnection closed normally. Only reconnect if the application logic requires a persistent connection.
1001YesServer is going away. Reconnect with a 5-30 second randomized delay.
1002NoProtocol error. Fix the client code before reconnecting. Retrying will produce the same error.
1003NoThe server does not accept your data type. Fix the client.
1006YesConnection lost. Reconnect with exponential backoff starting at 1 second.
1008MaybePolicy violation. If this is an auth issue, refresh credentials first. Otherwise, do not retry.
1009NoYour messages are too large. Reduce message size before reconnecting.
1010NoExtension negotiation failed. Fix the client configuration.
1011YesServer error. Reconnect with backoff. The server may have recovered.
1012YesServer restarting. Reconnect with a 5-30 second randomized delay.
1013YesServer overloaded. Reconnect with a longer delay (30-60 seconds).
1014YesUpstream failure. Reconnect with backoff.
4000-4999DependsApplication-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 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.