Skip to content
Control Plane Labs

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/socket

What 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.

Other 1xx codes