WebSocket HTTP Headers: Complete Handshake Reference
The Full Handshake
Section titled “The Full Handshake”Before covering each header, here is a complete handshake with every header you will encounter:
GET /chat HTTP/1.1Host: example.comUpgrade: websocketConnection: UpgradeSec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==Sec-WebSocket-Version: 13Sec-WebSocket-Protocol: graphql-ws, mqttSec-WebSocket-Extensions: permessage-deflate; client_max_window_bitsOrigin: https://example.comHTTP/1.1 101 Switching ProtocolsUpgrade: websocketConnection: UpgradeSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=Sec-WebSocket-Protocol: graphql-wsSec-WebSocket-Extensions: permessage-deflateAfter the server sends this response, both sides speak WebSocket binary frames. No more HTTP.
Client Request Headers
Section titled “Client Request Headers”Standard HTTP host header. Required by HTTP/1.1, not specific to WebSocket.
Who sets it: The browser or HTTP client, automatically.
What breaks: Without Host, the request is invalid HTTP. Most
servers return 400 Bad Request before WebSocket processing even
starts.
Upgrade: websocket
Section titled “Upgrade: websocket”Tells the server to switch protocols from HTTP to WebSocket.
Who sets it: The browser or client library, automatically.
What breaks: Without this header, the server processes the
request as a normal HTTP GET. You get back a 200 OK with an HTML
page or a 404 — not a WebSocket connection. The browser fires
onerror and onclose immediately.
Connection: Upgrade
Section titled “Connection: Upgrade”Signals that this is a hop-by-hop connection upgrade, not a regular request.
Who sets it: The browser or client library, automatically.
What breaks: Same as missing Upgrade. The server ignores
the upgrade intent. This header is also the one most commonly
stripped by reverse proxies — see
proxy issues below.
Sec-WebSocket-Key
Section titled “Sec-WebSocket-Key”A base64-encoded 16-byte random value. The server uses this to prove it actually understands the WebSocket protocol.
Who sets it: The browser generates 16 random bytes and base64-encodes them. Server-side clients do the same.
What breaks: If missing, the server cannot compute
Sec-WebSocket-Accept and must reject the handshake. If the value
is not valid base64 or not 16 bytes decoded, conforming servers
reject it. In practice, most libraries generate this correctly —
you will only hit issues with hand-rolled HTTP clients.
Example value: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Section titled “Sec-WebSocket-Version: 13”Declares the WebSocket protocol version. The only valid value is
13, defined in RFC 6455.
Versions 8 and below are obsolete drafts from 2011.
Who sets it: The browser, automatically. Always 13.
What breaks: If the client sends a different version, the
server must reject with 426 Upgrade Required and include
Sec-WebSocket-Version: 13 in the response to tell the client
what to use. Every modern browser and library sends 13. You will
only see version mismatches with very old or custom clients.
Sec-WebSocket-Protocol (optional)
Section titled “Sec-WebSocket-Protocol (optional)”A comma-separated list of application-level subprotocols the client supports. The server picks one.
Who sets it: Your code, via the second argument to the
WebSocket constructor:
const ws = new WebSocket('wss://example.com/chat', [ 'graphql-ws', 'graphql-transport-ws',]);// Sends: Sec-WebSocket-Protocol: graphql-ws, graphql-transport-wsWhat breaks: If the server does not support any listed subprotocol, it can either omit the header (connection opens without an agreed subprotocol) or reject the handshake entirely. Behavior depends on the server implementation. If the server returns a subprotocol the client did not request, the browser closes the connection.
Common subprotocols:
| Subprotocol | Use |
|---|---|
graphql-ws | GraphQL subscriptions (graphql-ws library) |
graphql-transport-ws | GraphQL subscriptions (older protocol) |
mqtt | MQTT over WebSocket |
wamp.2.json | WAMP v2 with JSON serialization |
ocpp1.6 | Open Charge Point Protocol (EV charging) |
Sec-WebSocket-Extensions (optional)
Section titled “Sec-WebSocket-Extensions (optional)”Requests protocol-level extensions, most commonly compression.
Who sets it: The browser or client library. You cannot control
this directly from the browser WebSocket API — browsers decide
whether to request permessage-deflate on their own.
Example:
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bitsWhat breaks: If the server does not support the requested extension, it omits it from the response. The connection proceeds without it. No error occurs.
The trade-off with permessage-deflate: compression saves
bandwidth but costs CPU and memory. The zlib sliding window
consumes ~300 KB per connection by default (two zlib sliding windows,
one per direction). At 10,000 connections, that is 3 GB of memory
just for compression state. For messages
under 100 bytes, compression often makes them larger. Skip it
unless your messages are consistently over 1 KB.
Origin
Section titled “Origin”The origin of the page that initiated the WebSocket connection.
Who sets it: The browser, automatically. You cannot override it from JavaScript. Server-side clients typically do not send it.
What breaks: Nothing breaks if Origin is missing — the
connection still opens. But if your server does not validate
Origin, any website can open WebSocket connections to your server
from a user’s browser, using their cookies and session. This is
Cross-Site WebSocket Hijacking (CSWSH), the WebSocket equivalent
of CSRF. Always validate Origin on the server against an
allowlist.
Server Response Headers
Section titled “Server Response Headers”The server must return HTTP/1.1 101 Switching Protocols with
these headers. Any other status code means the handshake failed.
Upgrade: websocket
Section titled “Upgrade: websocket”Confirms the protocol switch. Must match the client’s request.
Connection: Upgrade
Section titled “Connection: Upgrade”Confirms the connection upgrade. Must be present.
Sec-WebSocket-Accept
Section titled “Sec-WebSocket-Accept”The proof that the server understood the WebSocket upgrade. The
server computes this from the client’s Sec-WebSocket-Key:
- Concatenate
Sec-WebSocket-Keywith the magic string258EAFA5-E914-47DA-95CA-C5AB0DC85B11 - Compute the SHA-1 hash of the concatenated string
- Base64-encode the hash
Key: dGhlIHNhbXBsZSBub25jZQ==Concat: dGhlIHNhbXBsZSBub25jZQ==258EAFA5-E914-47DA-95CA-C5AB0DC85B11SHA-1: b37a4f2cc0624f1690f64606cf385945b2bec4eaBase64: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=If the value is wrong, the browser closes the connection immediately. This mechanism does not provide security — it is not encryption or authentication. It only proves the server intentionally processed the upgrade rather than being a caching proxy that blindly forwarded the request.
Sec-WebSocket-Protocol (conditional)
Section titled “Sec-WebSocket-Protocol (conditional)”If the client proposed subprotocols, the server returns the one it selected. The server must return exactly one value, not a list. If the server returns a subprotocol the client did not offer, the browser rejects the connection.
Sec-WebSocket-Extensions (conditional)
Section titled “Sec-WebSocket-Extensions (conditional)”The extensions the server agreed to. The server can accept all, some, or none of the client’s requested extensions.
The Sec- Prefix: Why It Exists
Section titled “The Sec- Prefix: Why It Exists”Headers starting with Sec- have a special rule: browsers prevent
JavaScript from setting or reading them. Only the browser engine
itself can set Sec-WebSocket-Key, Sec-WebSocket-Version, and
Sec-WebSocket-Extensions.
This matters because without this restriction, a malicious script
could forge a WebSocket handshake using XMLHttpRequest or
fetch() by manually setting the Upgrade and Sec-WebSocket-*
headers. The Sec- prefix makes this impossible — if a header
starts with Sec-, the browser silently drops any attempt by
JavaScript to set it.
Server-side clients (Node.js, Python, Go) are not bound by this restriction. They can set any header they want.
Headers You Cannot Set from the Browser
Section titled “Headers You Cannot Set from the Browser”The browser WebSocket API is deliberately minimal. The
constructor takes a URL and an optional subprotocol list. That is
it. You cannot set:
Authorization— No way to pass a Bearer token- Custom headers — No
X-Request-ID, noX-API-Key Cookie— You cannot choose which cookies to send (the browser sends all cookies for the domain automatically)
This is the single most common frustration developers hit when moving from REST to WebSocket in the browser.
Workarounds
Section titled “Workarounds”URL query parameters — The simplest approach. Put the token in the URL:
const ws = new WebSocket( 'wss://example.com/ws?token=eyJhbGciOi...');The downside: tokens in URLs appear in server access logs, proxy logs, and browser history. Use short-lived tokens and rotate them after connection establishment.
Cookies — If your WebSocket server shares a domain with your web app, authentication cookies are sent automatically. This works but ties your WebSocket auth to your HTTP session, which can be a problem when scaling across multiple server processes.
First-message authentication — Open the connection, then send credentials as the first message:
const ws = new WebSocket('wss://example.com/ws');ws.onopen = () => { ws.send(JSON.stringify({ type: 'auth', token: 'eyJhbGciOi...', }));};The server holds the connection but does not process other messages until it validates the token. This is what most real-time platforms use — services like Ably, Pusher, and PubNub all authenticate after the WebSocket connection is established rather than during the HTTP handshake.
Subprotocol header — Some developers encode tokens in the
Sec-WebSocket-Protocol header since it is the one header you
can set from the browser API. This works technically but abuses
the header’s purpose and can confuse debugging tools.
How Reverse Proxies Affect Headers
Section titled “How Reverse Proxies Affect Headers”Reverse proxies are the number one reason WebSocket connections fail in production. The connection works on localhost. It works when you connect directly to the server. It breaks the moment you put Nginx, HAProxy, or a cloud load balancer in front.
The problem: HTTP proxies treat Upgrade and Connection as
hop-by-hop headers. Per HTTP spec, hop-by-hop headers are consumed
by the first proxy and not forwarded. Your server never sees the
upgrade request. It gets a normal GET, returns a 200 or 404, and
the WebSocket handshake fails silently.
Nginx does not forward Upgrade by default. You must explicitly
pass it through:
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; proxy_send_timeout 86400s;}Three things go wrong if you skip parts of this config:
- Without
proxy_http_version 1.1, Nginx uses HTTP/1.0, which does not support connection upgrades at all. - Without the
UpgradeandConnectionheaders, the backend sees a normal GET request. - Without the timeout overrides, Nginx closes idle WebSocket
connections after 60 seconds (its default
proxy_read_timeout).
AWS Application Load Balancer
Section titled “AWS Application Load Balancer”ALB supports WebSocket natively on ports 80 and 443. No special
configuration is needed — it detects the Upgrade header and
switches to a persistent connection. However, ALB has an idle
timeout (default 60 seconds) that closes connections with no
traffic. Set the idle timeout higher or implement application-level
ping/pong.
Cloudflare
Section titled “Cloudflare”Cloudflare proxies WebSocket traffic automatically for all plans. The main gotcha: Cloudflare enforces a 100-second idle timeout on free plans. If your application has quiet periods longer than that, send WebSocket ping frames every 30 seconds.
Frequently Asked Questions
Section titled “Frequently Asked Questions”What headers are required for a WebSocket handshake?
Section titled “What headers are required for a WebSocket handshake?”Five client headers are mandatory: Host, Upgrade: websocket,
Connection: Upgrade, Sec-WebSocket-Key, and
Sec-WebSocket-Version: 13. The server must respond with
101 Switching Protocols, Upgrade: websocket,
Connection: Upgrade, and Sec-WebSocket-Accept. If any of
these are missing or wrong, the handshake fails — the browser
fires onerror and closes the connection. See the
full handshake above for a complete
example.
Can I set custom headers on a browser WebSocket connection?
Section titled “Can I set custom headers on a browser WebSocket connection?”No. The browser WebSocket constructor accepts only a URL and
an optional subprotocol list. There is no parameter for custom
headers. This means you cannot send Authorization,
X-API-Key, or any other custom header. Use URL query
parameters, cookies, or first-message authentication instead.
See workarounds for code examples.
What does the Sec- prefix mean on WebSocket headers?
Section titled “What does the Sec- prefix mean on WebSocket headers?”The Sec- prefix marks headers that only the browser engine can
set. JavaScript cannot create, modify, or read Sec- headers
through XMLHttpRequest or fetch(). This prevents malicious
scripts from forging WebSocket handshakes. Server-side clients
are not restricted — they can set any header. See
why the Sec- prefix exists
for details.
Why does my WebSocket connection fail behind Nginx?
Section titled “Why does my WebSocket connection fail behind Nginx?”Nginx treats Upgrade and Connection as hop-by-hop headers
and strips them by default. Your backend receives a normal HTTP
GET, not a WebSocket upgrade request. Add
proxy_http_version 1.1, proxy_set_header Upgrade,
and proxy_set_header Connection "Upgrade" to your Nginx
location block. See the Nginx config above for a
working example.
What is Sec-WebSocket-Accept and how is it calculated?
Section titled “What is Sec-WebSocket-Accept and how is it calculated?”The server concatenates the client’s Sec-WebSocket-Key with
the magic GUID 258EAFA5-E914-47DA-95CA-C5AB0DC85B11, takes
the SHA-1 hash, and base64-encodes the result. This proves the
server intentionally processed the WebSocket upgrade. It is not
a security mechanism — it does not authenticate or encrypt
anything. It only prevents caching proxies from accidentally
completing a handshake they do not understand.
Related Content
Section titled “Related Content”- WebSocket API Reference — The browser API for creating and managing WebSocket connections
- WebSocket Close Codes — Status codes returned when a WebSocket connection closes
- What Are WebSockets? — How WebSockets work, from HTTP to persistent connections
- WebSocket Security Guide — TLS, authentication, and CSWSH prevention
- Nginx WebSocket Configuration — Reverse proxy setup for WebSocket traffic