Back to HTTP Headers

WebSocket Handshake Headers websocket both

The 5 headers that negotiate a WebSocket connection — turning an HTTP/1.1 connection into a persistent full-duplex WebSocket channel.

What the WebSocket handshake does

WebSocket starts as an HTTP/1.1 request. The client sends a special GET with Upgrade: websocket headers; the server agrees with 101 Switching Protocols. After that, the TCP connection is no longer HTTP — it's a WebSocket stream where both sides can send frames at any time.

The handshake serves three purposes:

  1. Upgrade the protocol — signal the intent to switch from HTTP to WebSocket
  2. Verify the handshake — the Key/Accept pair proves the server actually supports WebSocket (not just reflecting an HTTP response)
  3. Negotiate subprotocol and extensions — agree on application-level protocol and optional features

The complete handshake

Client request:

GET /chat HTTP/1.1
Host: ws.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
Origin: https://example.com

Server response:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat
Sec-WebSocket-Extensions: permessage-deflate

After 101, the HTTP layer ends and WebSocket frames flow both ways.

The 5 WebSocket-specific headers

Sec-WebSocket-Key

Direction: Client → Server

A random 16-byte value encoded as base64, generated fresh for each connection attempt:

Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==

Its purpose is not authentication — it's a handshake proof mechanism. The server transforms this key into Sec-WebSocket-Accept, proving it understood and processed the WebSocket upgrade request (not just echoed a cached HTTP response).

The Sec- prefix makes this a "forbidden header" — browsers prevent JavaScript from setting it manually, ensuring only the browser's WebSocket implementation sends it.

Sec-WebSocket-Accept

Direction: Server → Client

The server's response to Sec-WebSocket-Key. Computed by:

  1. Concatenating the key with the magic GUID 258EAFA5-E914-47DA-95CA-C5AB0DC85B11
  2. Taking the SHA-1 hash
  3. Base64-encoding the result
import hashlib, base64
key = "dGhlIHNhbXBsZSBub25jZQ=="
magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
accept = base64.b64encode(hashlib.sha1((key + magic).encode()).digest()).decode()
# → s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

The client verifies this value. A mismatch means the server didn't actually process the WebSocket handshake correctly — the connection is rejected.

Sec-WebSocket-Version

Direction: Client → Server (request), Server → Client (on rejection)

Specifies the WebSocket protocol version the client wants to use:

Sec-WebSocket-Version: 13

Version 13 (RFC 6455, 2011) is the only version in use. It superseded draft versions 7, 8, and earlier. You'll always see 13.

If the server doesn't support the requested version, it responds with 426 Upgrade Required and lists supported versions:

HTTP/1.1 426 Upgrade Required
Sec-WebSocket-Version: 13

Sec-WebSocket-Protocol

Direction: Client → Server (offers), Server → Client (selects one)

Application-level subprotocol negotiation. The client lists protocols it supports; the server picks one:

# Client offers:
Sec-WebSocket-Protocol: chat, superchat, json-rpc

# Server selects one:
Sec-WebSocket-Protocol: chat

Subprotocols are application-defined strings — there's no central registry for most of them. Common ones: graphql-ws, graphql-transport-ws, stomp, wamp, json. They tell the application what message format and semantics to use over the WebSocket connection.

If the server doesn't include Sec-WebSocket-Protocol in the response, no subprotocol was negotiated — the connection is untyped.

Sec-WebSocket-Extensions

Direction: Client → Server (offers), Server → Client (selects/configures)

Negotiates WebSocket protocol extensions. The most common extension is permessage-deflate — per-message compression:

# Client offers:
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits

# Server accepts with configuration:
Sec-WebSocket-Extensions: permessage-deflate; server_max_window_bits=15

permessage-deflate compresses each WebSocket message with deflate, significantly reducing bandwidth for text-heavy protocols (JSON, XML). Both sides must agree — if the server doesn't include the extension in its response, messages are uncompressed.

Server implementation notes

Most WebSocket server libraries handle the handshake automatically. If you're implementing it from scratch:

// Node.js manual handshake
const crypto = require('crypto');

function generateAccept(key) {
  const GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
  return crypto
    .createHash('sha1')
    .update(key + GUID)
    .digest('base64');
}

// On receiving upgrade request:
const key = req.headers['sec-websocket-key'];
const accept = generateAccept(key);

socket.write(
  'HTTP/1.1 101 Switching Protocols\r\n' +
  'Upgrade: websocket\r\n' +
  'Connection: Upgrade\r\n' +
  `Sec-WebSocket-Accept: ${accept}\r\n` +
  '\r\n'
);

Proxying WebSockets

WebSocket connections through nginx require explicit configuration because the Upgrade and Connection headers are hop-by-hop and stripped by default:

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;
    proxy_read_timeout 86400s;  # Don't time out long-lived connections
}

Without proxy_http_version 1.1 and the Upgrade/Connection headers being forwarded, nginx won't proxy WebSocket connections.

Common mistakes and gotchas

Not forwarding Upgrade/Connection through proxies. The most common WebSocket deployment issue. nginx and other reverse proxies strip hop-by-hop headers by default. Always add the proxy config above.

Using WebSocket over HTTP/2 incorrectly. Standard Upgrade: websocket doesn't work in HTTP/2. Use the RFC 8441 CONNECT method with :protocol: websocket pseudo-header for WebSocket over HTTP/2. Most WebSocket clients negotiate HTTP/1.1 specifically for WebSocket connections.

Forgetting proxy_read_timeout on long-lived connections. nginx's default read timeout is 60 seconds. A WebSocket connection that's idle for 60 seconds will be killed. Set a long timeout (or use WebSocket ping/pong to keep the connection alive).

Not validating Origin on the server. The browser sends Origin on WebSocket upgrade requests. A server that accepts connections from any origin is vulnerable to cross-site WebSocket hijacking — a script on evil.com can open a WebSocket to ws.yourapp.com using the user's cookies. Always validate Origin against an allowlist.

Real-world examples

Minimal chat WebSocket:

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

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

With subprotocol and compression:

GET /graphql HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: abc123...
Sec-WebSocket-Version: 13
Sec-WebSocket-Protocol: graphql-transport-ws
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: xyz789...
Sec-WebSocket-Protocol: graphql-transport-ws
Sec-WebSocket-Extensions: permessage-deflate

FAQ

Why is the magic GUID so weird?

The GUID 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 was chosen to be unique enough that it wouldn't appear in random data. Its specific value was picked by the RFC 6455 authors specifically to be bizarre enough that no pre-existing protocol accidentally uses it. It's a collision-avoidance mechanism, not a secret.

Can I use WebSocket with HTTP/2?

Yes, via RFC 8441 (bootstrapping WebSocket with HTTP/2). The client sends a CONNECT request with :protocol: websocket as a pseudo-header. It's more efficient than negotiating down to HTTP/1.1 for WebSocket. Browser support arrived in Chrome 67 and Firefox 68. Many WebSocket libraries still default to HTTP/1.1 for simplicity.

What happens if the WebSocket handshake fails?

The connection stays as HTTP. The server returns a regular HTTP error (400, 426, etc.) and the connection is not upgraded. The WebSocket client receives an error event.

Fun fact

The SHA-1 Sec-WebSocket-Key / Sec-WebSocket-Accept challenge-response was designed to prevent a specific attack: a network intermediary caching an HTTP response and returning it when the client tried to upgrade to WebSocket. Without the key/accept verification, the client might think it had established a WebSocket connection when it was actually just talking to a cached HTTP response. The magic GUID ensures the server had to actually run the SHA-1 computation — something a dumb HTTP cache would never do. SHA-1 is cryptographically weak today, but that doesn't matter here — the goal is proof of processing, not cryptographic security.

Related Headers