101 Switching Protocols
Informational response defined by RFC 9110 §15.2.2.
Last updated July 27, 2026
Common causes at a glance
- A successful WebSocket handshake — the normal, healthy case
- An HTTP/1.1 client requesting an Upgrade the server supports
- Confusion when a proxy or CDN in front of the app does not forward Upgrade headers
- A client speaking HTTP/2 to an endpoint that only knows the HTTP/1.1 upgrade dance
Reproduce it
curl -i -N -H 'Connection: Upgrade' -H 'Upgrade: websocket' -H 'Sec-WebSocket-Version: 13' -H 'Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==' https://ws.example.com/socketWhat 101 Switching Protocols means
101 is the handoff. The client sends an ordinary HTTP/1.1 request with
Connection: Upgrade and Upgrade: websocket; the server
answers 101, and from the byte after the blank line the connection is no longer
speaking HTTP. Nothing about the connection is reusable for further HTTP requests,
which is why 101 responses have no body and why intermediaries that assume
request-response framing break when they sit in front of one.
The other historical use was upgrading cleartext HTTP to TLS, which nobody deploys.
What the spec says
RFC 9110 §15.2.2 defines 101 and requires the
response to include an Upgrade header naming the new protocol.
RFC 9110 §7.8 defines the Upgrade field itself. The
WebSocket-specific handshake, including the Sec-WebSocket-Key and
Sec-WebSocket-Accept computation, is
RFC 6455 §4. Note that the upgrade mechanism is an HTTP/1.1
feature: HTTP/2 dropped it, and WebSockets over HTTP/2 use extended
CONNECT instead (RFC 8441).
What actually causes it
The interesting cases are when 101 does not arrive. A
reverse proxy that does not forward the Upgrade and
Connection headers will turn the handshake into a plain HTTP request and
you will see 200, 400, or 426 instead. nginx needs both headers set explicitly on the
proxied request, and the connection header has to be upgrade rather than
the inherited close. Kubernetes ingress controllers, ALBs, and service
meshes each have their own switch for this. If your client negotiated HTTP/2 via ALPN,
the upgrade token is simply not available and you need RFC 8441 support on both ends.
How to debug it
Reproduce the handshake with curl and read the raw response. A healthy
exchange returns HTTP/1.1 101 Switching Protocols plus
Upgrade: websocket and a Sec-WebSocket-Accept value derived
from the key you sent. If you get 200, something terminated the request as ordinary
HTTP. If you get 426, the server wants an upgrade you did not offer.
Force HTTP/1.1 to take protocol negotiation out of the picture:
curl -i -N --http1.1 -H 'Connection: Upgrade' -H 'Upgrade: websocket' \
-H 'Sec-WebSocket-Version: 13' \
-H 'Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==' \
https://ws.example.com/socket
For that fixed key the correct Sec-WebSocket-Accept is
s3pPLMBiTxaQ9kYGzzhZRbK+xOo=; if you get something else, an
intermediary rewrote the key. Then check the hop-by-hop config with the
header inspector.
Working examples
curl
# --http1.1 matters: the Upgrade mechanism does not exist in HTTP/2.
# -N disables buffering so you see the 101 immediately.
curl -i -N --http1.1 \
-H 'Connection: Upgrade' \
-H 'Upgrade: websocket' \
-H 'Sec-WebSocket-Version: 13' \
-H 'Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==' \
https://ws.example.com/socket
Python (requests)
import requests
# A raw handshake probe. requests will hand back the 101 without upgrading,
# which is exactly what you want when testing proxy configuration.
resp = requests.get(
"https://ws.example.com/socket",
headers={
"Connection": "Upgrade",
"Upgrade": "websocket",
"Sec-WebSocket-Version": "13",
"Sec-WebSocket-Key": "dGhlIHNhbXBsZSBub25jZQ==",
},
timeout=10,
)
print(resp.status_code, resp.headers.get("Sec-WebSocket-Accept"))
# expect: 101 s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Node (fetch)
// WebSocket in Node hides the 101, so listen for the upgrade event directly.
import http from "node:http";
const req = http.request({
host: "ws.example.com", path: "/socket",
headers: {
Connection: "Upgrade", Upgrade: "websocket",
"Sec-WebSocket-Version": "13",
"Sec-WebSocket-Key": "dGhlIHNhbXBsZSBub25jZQ==",
},
});
req.on("upgrade", (res, socket) => {
console.log(res.statusCode, res.headers["sec-websocket-accept"]);
socket.destroy();
});
req.on("response", (res) => console.log("no upgrade, got", res.statusCode));
req.end();Reference and tooling
The authoritative list of assigned status codes is theIANA HTTP Status Code Registry. For a searchable copy with the semantics summarised, use theHTTP status code reference on this site. To see exactly what a server is sending, theheader inspector shows the raw status line and response headers, and thecurl builder assembles the request flags for you.