WebSockets Behind a CDN: What Works and What Breaks

A WebSocket upgrade turns a CDN edge into a bidirectional tunnel. The moment your origin answers the RFC 6455 handshake with 101 Switching Protocols, caching, compression, image transforms, range requests and request collapsing stop applying to that connection for its entire life. WebSocket CDN support is therefore judged almost entirely by handshake behavior and timeout policy: idle timeouts documented in the 60–100 second range as of 2026, per-connection memory of roughly 300 KB once permessage-deflate negotiates context takeover, and a 40 ms delayed-ACK stall waiting for anyone who forgot to disable Nagle.

What does survive is still worth having: TLS termination close to the user, anycast path selection, a warm pooled leg to origin, header rewriting and authentication at handshake time, and rate limiting on the handshake request. The mistake is assuming the rest of the feature set follows the connection through the upgrade. It does not, and the failure modes show up at 10,000 concurrent connections, not at 100.

Diagram-style illustration of websocket cdn traffic flowing from clients through edge nodes to origin for real-time applications

WebSocket CDN behavior: what survives the upgrade and what disappears

The handshake is an ordinary HTTP/1.1 request, so every edge feature that operates on requests gets exactly one shot at it. After the 101, the edge is copying opaque frames in both directions and has no protocol-level idea what is inside them.

CDN capabilitySurvives the upgrade?Practical consequence
TLS termination at the edgeYesHandshake RTT savings still apply; this is the main real win
Anycast routing and backbone transitYesFewer lossy hops on the long leg, steadier frame jitter
Cache (any tier, including shield)NoHit ratio on this hostname is structurally 0%; egress is 100% billable
Edge compression (gzip, Brotli)NoFrame compression must be negotiated per RFC 7692 instead
Request collapsing, retries, failover on errorHandshake onlyMid-stream origin failure becomes a client-visible disconnect
Access rules, token auth, header rewriteHandshake onlyAuthorization is evaluated once, then the tunnel is trusted for hours
Per-request access logsNoOne log line per connection, not per message; p99 latency is invisible

The single most important conclusion: a CDN in front of WebSockets buys you network path and TLS, and nothing else, so price and evaluate that hostname purely as transit.

Why HTTP/2 and HTTP/3 barely help WebSocket CDN traffic

RFC 8441 defines WebSockets over HTTP/2 using extended CONNECT, and RFC 9220 extends the same mechanism to HTTP/3. Browser and edge support exists but is uneven, and most edges still downgrade the upgrade to HTTP/1.1 on the origin leg. That means one TCP connection per WebSocket to origin, no stream multiplexing, and no head-of-line-blocking benefit from QUIC on the part of the path where you would most want it.

There is a second-order effect worth noting. Under HTTP/1.1 tunnelling, each client connection consumes an origin-side socket for its full lifetime, so a 250,000-connection fleet needs the ephemeral port and file-descriptor math done per origin IP tuple, not per request rate.

What buffering does to real-time traffic

Two independent buffers hurt. The first is proxy buffering: if any layer accumulates bytes before forwarding, small frames get coalesced and delivery becomes bursty. The second is TCP itself. Nagle's algorithm plus Linux delayed ACK, whose minimum timer is 40 ms, will hold a small write until an ACK arrives, and that penalty applies at both the edge-to-origin and edge-to-client hop.

For a 20 Hz game state stream with 50 ms between frames, a 40 ms stall is close to a full tick. Measure it as the gap between application-level ping RTT and raw TCP RTT; if application RTT p99 exceeds TCP RTT p99 by 30–45 ms with no queueing on the origin, you are looking at a buffering problem, not a capacity problem.

The 300 KB problem: permessage-deflate at scale

Per RFC 7692, permessage-deflate with context takeover keeps a live zlib deflate context per connection per direction, which at the default 15 window bits costs roughly 300 KB of memory for the compressor and about 44 KB for the decompressor. At 100,000 concurrent WebSocket connections that is on the order of 30 GB of RAM before a single message is queued, which is why many real-time platforms in 2026 either negotiate no_context_takeover, cap window bits at 10–12, or disable the extension entirely for small-payload traffic.

The trade-off is honest: context takeover typically compresses repetitive JSON payloads 60–85%, and switching it off raises egress on exactly the traffic that gets no cache offload. Small frames under about 200 bytes rarely compress usefully anyway, so a per-message size threshold usually beats a global on/off decision.

Timeouts, reconnect storms, and the observability gap

Every edge enforces an idle timeout on a tunnelled connection. Vendor-published values commonly sit in the 60–100 second band as of 2026, which means application-level ping frames every 20–30 seconds are mandatory, not optional. Relying on TCP keepalive alone fails because the default first probe on Linux fires after 7,200 seconds.

The bigger risk is correlated disconnection. An edge configuration deploy, a node drain, or a routing change drops every connection it holds at once. If 100,000 clients reconnect with a fixed 1-second retry, the origin sees 100,000 handshakes per second, each requiring authentication and state rehydration. Full jitter over a 5–30 second window turns that into roughly 4,000 handshakes per second, and a token bucket on the handshake route caps the worst case.

Observability is the gap nobody plans for. CDN logs give you connection open, close, duration and bytes; they cannot give you message latency, frame counts, or which shard was slow. That instrumentation has to live in your application, emitted as histograms keyed by edge node identifier so you can attribute regressions to a region.

Designing around the limits

Split the planes. Put cacheable assets, API reads and manifests on the hostname where cache, compression and collapsing earn their keep, and put the real-time tunnel on a dedicated hostname with caching off, edge compression off, buffering off and long read timeouts. Separate hostnames let you tune each one honestly instead of compromising both.

upstream ws_upstream {
    server 10.0.0.11:8080 max_fails=2 fail_timeout=5s;
    server 10.0.0.12:8080 max_fails=2 fail_timeout=5s;
    keepalive 64;
}

map $http_upgrade $connection_upgrade {
    default upgrade;
    ""      close;
}

server {
    listen 443 ssl;
    server_name REALTIME_HOSTNAME;

    location /ws/ {
        proxy_pass http://ws_upstream;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection $connection_upgrade;
        proxy_set_header X-Edge-Node EDGE_NODE_ID;
        proxy_buffering off;
        proxy_request_buffering off;
        proxy_read_timeout 300s;
        proxy_send_timeout 300s;
        tcp_nodelay on;
    }
}

Substitute REALTIME_HOSTNAME with the dedicated real-time hostname and EDGE_NODE_ID with whatever node identifier your edge injects, so application metrics can be grouped by node. If a provider cannot disable buffering and compression per hostname, that is a hard limit on how well real-time traffic behaves through it; flexible per-property configuration is one of the few things worth checking against the CDN feature set for real-time and WebSocket traffic before you commit a hostname to it.

Verify this on your own stack

  • Run a handshake with a WebSocket client that prints response headers and confirm you get 101 with Connection: Upgrade intact, and check whether Sec-WebSocket-Extensions came back with permessage-deflate and any window-bits limits.
  • Open one connection, send nothing, and record how long it lives. That number is your real idle timeout, regardless of documentation.
  • Compare application ping RTT p99 against TCP RTT p99 from the same host; a gap above 30 ms points at buffering or Nagle.
  • Check your CDN billing report for that hostname: cache hit ratio should be 0% and every byte billable, which is the correct baseline for a real-time traffic CDN decision.

Who should put WebSockets behind a CDN

It fits when clients are globally distributed and the long haul is lossy: TLS at the edge plus managed transit measurably steadies frame jitter for chat, collaboration, dashboards and presence systems. It also fits when you want handshake-time authentication and rate limiting enforced off your origin.

It fits poorly when connections are regional and already low-latency, when you need mid-stream failover, or when message volume is high and compression choices are dictated by the edge rather than by you. Sub-20 ms interactive workloads and anything needing unreliable datagrams are better served by a direct path or WebTransport over HTTP/3.

FAQ: WebSocket CDN support and real-time traffic

Does CDN WebSocket support include caching or compression?

No. After the 101 Switching Protocols response, the edge treats the connection as an opaque tunnel, so caching, edge compression and request collapsing no longer apply. Cache hit ratio on a WebSocket hostname is structurally 0%. Frame-level compression must be negotiated between client and origin using permessage-deflate as defined in RFC 7692.

What is the maximum WebSocket connection duration behind a CDN?

Most edges do not cap total duration but do enforce an idle timeout, commonly in the 60–100 second range as of 2026, plus occasional forced closes during configuration deploys or node drains. Send application-level ping frames every 20–30 seconds and treat any disconnect as expected, with jittered reconnect logic on the client.

Should real-time traffic bypass the CDN entirely?

Bypass makes sense when clients are concentrated in one or two regions and baseline RTT is already under 30 ms, since the edge adds a hop without offering cache offload. Keep the CDN path when users are global, packet loss on transit is material, or you want handshake authentication and rate limiting applied before origin.

Is WebSocket over HTTP/2 and HTTP/3 worth enabling in 2026?

It helps on the client leg by removing one TCP and TLS handshake per connection, using extended CONNECT per RFC 8441 and RFC 9220. The benefit is limited because most edges still downgrade to HTTP/1.1 toward origin, so origin socket counts and head-of-line behavior on that leg stay unchanged.

Instrument the gap this week

Pick your busiest real-time hostname and emit two histograms for seven days: application ping RTT and TCP RTT from the same process, both tagged with the edge node identifier and the client region. Then plot idle-close events against your ping interval. Most teams discover one of three things — a 40 ms buffering tax, a ping interval sitting uncomfortably close to the edge idle timeout, or reconnect bursts clustered within a two-second window after a deploy. All three are cheap to fix once you can see them, and none of them are visible in CDN logs alone.