tests.ws

WebSocket Handshake - How It Works

websocket handshake http upgrade websocket protocol websocket headers

The WebSocket handshake is the process that upgrades a standard HTTP connection into a persistent, full-duplex WebSocket connection. It starts with the client sending a special HTTP request, and the server responding with a status code that confirms the protocol switch. Once complete, both sides communicate through WebSocket frames instead of HTTP request-response pairs.

If you have worked with WebSockets before, you know they feel almost magical compared to traditional HTTP polling. But that magic starts with a very specific, well-defined handshake sequence built on top of HTTP/1.1. Understanding this sequence helps you debug connection issues, configure proxies correctly, and build more reliable real-time applications.

The HTTP Upgrade Request

Every WebSocket connection begins as a normal HTTP GET request. The client sends a set of required headers that signal its intent to upgrade the connection. Here is what a typical client handshake request looks like:

GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Origin: https://example.com

Each header serves a specific purpose. The following table breaks them down:

HeaderValuePurpose
GET /chat HTTP/1.1Request lineSpecifies the endpoint and HTTP version. Must be HTTP/1.1 or later. The path (/chat) is the WebSocket endpoint on the server.
Hostexample.comStandard HTTP host header. Required for routing, especially when multiple domains share an IP address.
UpgradewebsocketTells the server that the client wants to switch protocols to WebSocket.
ConnectionUpgradeSignals that this is a connection upgrade request, not a regular HTTP request.
Sec-WebSocket-KeyBase64-encoded 16-byte random valueA one-time nonce the server uses to prove it understood the WebSocket upgrade request. The client generates a random 16-byte value and base64-encodes it.
Sec-WebSocket-Version13The WebSocket protocol version. Version 13 is defined in RFC 6455 and is the only version in widespread use today.
Originhttps://example.comThe origin of the page initiating the connection. Servers can use this to accept or reject connections from specific domains.

A few things to note about the request. The method must be GET. POST, PUT, and other HTTP methods will not work. The HTTP version must be 1.1, because the upgrade mechanism is defined in that version of the spec. The Sec-WebSocket-Key is not encryption or authentication. It simply prevents caching proxies from replaying old WebSocket handshake responses.

The Server Response

When the server accepts the upgrade request, it responds with HTTP status code 101 Switching Protocols:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

The critical header here is Sec-WebSocket-Accept. The server computes this value from the client’s Sec-WebSocket-Key using a specific algorithm defined in RFC 6455.

How Sec-WebSocket-Accept Is Computed

The computation follows these exact steps:

  1. Take the client’s Sec-WebSocket-Key value: dGhlIHNhbXBsZSBub25jZQ==
  2. Concatenate it with the magic GUID string 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 (this string is hardcoded in the RFC and never changes).
  3. Compute the SHA-1 hash of the concatenated string.
  4. Base64-encode the resulting hash.

Here is the actual computation:

Input:  dGhlIHNhbXBsZSBub25jZQ==258EAFA5-E914-47DA-95CA-C5AB0DC85B11
SHA-1:  b3 7a 4f 2c c0 62 4f 16 90 f6 46 06 cf 38 59 45 b2 be c4 ea
Base64: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

You can verify this yourself using command-line tools:

echo -n "dGhlIHNhbXBsZSBub25jZQ==258EAFA5-E914-47DA-95CA-C5AB0DC85B11" \
  | openssl sha1 -binary \
  | openssl base64

This outputs s3pPLMBiTxaQ9kYGzzhZRbK+xOo=, which matches the Sec-WebSocket-Accept value in the server response.

The purpose of this computation is not security. It proves that the server actually understands the WebSocket protocol rather than blindly accepting any upgrade request. It also prevents HTTP caching proxies from confusing a cached response with a valid handshake.

What Happens After the Handshake

Once the server sends the 101 response and the client validates the Sec-WebSocket-Accept value, the HTTP connection is done. The underlying TCP socket stays open, but both sides now speak the WebSocket frame protocol instead of HTTP.

All further communication happens through WebSocket frames. These frames have a binary header that includes an opcode (text, binary, ping, pong, or close), a payload length, and optional masking. Client-to-server frames are always masked with a 4-byte masking key. Server-to-client frames are never masked.

The transition is immediate. There is no additional negotiation after the handshake completes. The very next bytes on the wire after the HTTP response are WebSocket frame data.

Common Handshake Failures

When the handshake fails, you typically see one of these HTTP status codes or connection-level errors:

HTTP 400 Bad Request

The server received the request but found it malformed. Common causes:

  • Missing Upgrade: websocket header.
  • Missing or invalid Sec-WebSocket-Key.
  • Unsupported Sec-WebSocket-Version (the server may respond with a Sec-WebSocket-Version header listing the versions it supports).
  • Invalid request path or query parameters.

HTTP 403 Forbidden

The server understood the request but refused it. This usually means:

  • The Origin header did not match the server’s allowlist.
  • Authentication failed (missing or invalid cookies, tokens, or API keys).
  • Rate limiting or IP-based blocking is in effect.

For WebSocket security best practices, always validate the Origin header on the server side to prevent cross-site WebSocket hijacking.

HTTP 426 Upgrade Required

The server requires a different protocol version. This is uncommon with modern clients since version 13 is universally supported, but older clients or misconfigured libraries can trigger it.

Connection Refused or Timeout

These are not HTTP errors. They happen at the TCP level:

  • The server is not running or not listening on the expected port.
  • A firewall is blocking the connection.
  • DNS resolution failed.

Proxy and Load Balancer Issues

Reverse proxies and load balancers are a frequent source of handshake failures. Many proxies do not forward the Upgrade and Connection headers by default. If you run Nginx in front of your WebSocket server, you need explicit configuration:

location /ws/ {
    proxy_pass http://backend;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "Upgrade";
    proxy_set_header Host $host;
}

Without proxy_http_version 1.1, Nginx uses HTTP/1.0 for upstream connections, which does not support the upgrade mechanism. Without the Upgrade and Connection header forwarding, the backend server never sees the client’s upgrade request.

Some cloud load balancers also impose idle timeouts that close WebSocket connections after a period of inactivity. Configure ping/pong frames or application-level heartbeats to keep connections alive.

Subprotocols and Extensions

The handshake supports two optional negotiation mechanisms: subprotocols and extensions.

Sec-WebSocket-Protocol (Subprotocols)

The client can request one or more application-level subprotocols during the handshake:

GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: x3JJHMbDL1EzLkh9GBhXDw==
Sec-WebSocket-Version: 13
Sec-WebSocket-Protocol: graphql-transport-ws, graphql-ws

The server picks one and includes it in the response:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: HSmrc0sMlYUkAGmm5OPpG2HaGWk=
Sec-WebSocket-Protocol: graphql-transport-ws

Subprotocols define how messages are structured on top of WebSocket. Common examples include graphql-transport-ws for GraphQL subscriptions, mqtt for IoT messaging, and wamp for the Web Application Messaging Protocol. If the server does not support any of the requested subprotocols, it can either omit the header (and the client decides whether to continue) or reject the connection.

Sec-WebSocket-Extensions (Extensions)

Extensions modify the WebSocket protocol itself, typically at the framing level. The most common extension is permessage-deflate, which compresses message payloads:

Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits

The server responds with the negotiated extension parameters:

Sec-WebSocket-Extensions: permessage-deflate; server_no_context_takeover; client_max_window_bits=15

The permessage-deflate extension applies zlib compression to each message, reducing bandwidth usage significantly for text-heavy payloads. The parameters control window sizes and context reuse. Be aware that compression adds CPU overhead, so it is not always beneficial for small messages or high-throughput scenarios.

Handshake in Code

Browser JavaScript (Automatic Handshake)

In the browser, the WebSocket API handles the entire handshake automatically. You do not construct upgrade headers manually:

const socket = new WebSocket("wss://example.com/chat", ["graphql-transport-ws"]);

socket.addEventListener("open", (event) => {
  console.log("Handshake complete, connection open");
  console.log("Negotiated protocol:", socket.protocol);
});

socket.addEventListener("error", (event) => {
  console.error("Handshake or connection failed");
});

The browser generates the Sec-WebSocket-Key, sends all required headers, validates the server’s Sec-WebSocket-Accept, and fires the open event when the handshake succeeds. You cannot modify the upgrade headers directly from browser JavaScript. If you need custom headers (like authentication tokens), pass them as query parameters or use cookies.

For a full walkthrough of client-side WebSocket programming, see the JavaScript WebSocket guide.

Node.js with the ws Library (Manual Inspection)

On the server side, you have full control over the handshake. The popular ws library for Node.js lets you inspect and modify the upgrade process:

import { WebSocketServer } from "ws";
import http from "http";

const server = http.createServer();
const wss = new WebSocketServer({ noServer: true });

server.on("upgrade", (request, socket, head) => {
  // Inspect handshake headers
  const origin = request.headers["origin"];
  const protocols = request.headers["sec-websocket-protocol"];

  // Reject unauthorized origins
  if (origin !== "https://example.com") {
    socket.write("HTTP/1.1 403 Forbidden\r\n\r\n");
    socket.destroy();
    return;
  }

  // Complete the handshake
  wss.handleUpgrade(request, socket, head, (ws) => {
    wss.emit("connection", ws, request);
  });
});

wss.on("connection", (ws, request) => {
  console.log("Client connected from:", request.headers["origin"]);

  ws.on("message", (data) => {
    console.log("Received:", data.toString());
  });
});

server.listen(8080);

Using noServer: true with the upgrade event gives you full control over origin validation, authentication, and routing before the handshake completes. This pattern is especially useful when you need to route different WebSocket paths to different handlers or integrate with existing HTTP middleware.

Debugging the Handshake

When something goes wrong during the handshake, you have several tools at your disposal.

Browser DevTools

Chrome and Firefox DevTools show WebSocket connections in the Network tab. Filter by “WS” to see only WebSocket traffic. Click on a connection to inspect:

  • The request and response headers (including all Sec-WebSocket-* headers).
  • The HTTP status code (101 for success, or an error code).
  • Individual WebSocket frames after the handshake.

This is the fastest way to verify that your client is sending the correct headers and the server is responding as expected. You can also test connections with the WebSocket tester tool.

wscat

wscat is a command-line WebSocket client built on the ws library. It is useful for testing WebSocket endpoints without writing code:

# Install globally
npm install -g wscat

# Connect to a WebSocket server
wscat -c wss://example.com/chat

# Connect with a subprotocol
wscat -c wss://example.com/chat -s graphql-transport-ws

# Show HTTP headers during handshake
wscat -c wss://example.com/chat --show-ping-pong

curl

You can perform the WebSocket handshake manually with curl to see the raw HTTP exchange:

curl -i -N \
  -H "Connection: Upgrade" \
  -H "Upgrade: websocket" \
  -H "Sec-WebSocket-Version: 13" \
  -H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \
  https://example.com/chat

The -i flag shows response headers, and -N disables output buffering. If the handshake succeeds, you see the 101 response. If it fails, you see the error status code and any diagnostic headers the server included.

This approach is particularly valuable when debugging proxy configurations because you can see exactly what headers arrive at the server and what comes back.

Wireshark

For deep inspection, Wireshark can capture and decode WebSocket traffic at the packet level. Filter with websocket or tcp.port == 80 to isolate relevant traffic. Wireshark shows you the raw bytes of the handshake, frame headers, and payload data. This is your best option when you suspect data corruption, framing errors, or TLS negotiation problems.

Now that you understand how the WebSocket handshake works, explore these related topics:

  • What Is WebSocket covers the fundamentals of the WebSocket protocol, including why it exists and how it compares to HTTP.
  • WebSocket Security explains how to protect your WebSocket connections with origin validation, authentication, and TLS.
  • WebSocket Close Codes documents every standard close code and what each one means for your application.
  • JavaScript WebSocket Guide walks through building a full client-side WebSocket application with reconnection logic and error handling.
  • WebSocket Tester lets you connect to any WebSocket endpoint directly in your browser to send and receive messages.