Single-page applications that route 100% of their state hydration, telemetry, and AI chat streaming exclusively through a persistent WebSocket (wss://) assume a clean network path where TCP port 443 and HTTP/1.1 101 Switching Protocols handshakes succeed in under 100 milliseconds. In real-world production across East Africa and emerging markets, however, mobile and fiber ISPs periodically apply aggressive traffic shaping or deep packet inspection (DPI) rate limits on new TCP connections destined for shared CDN Anycast IP ranges—causing new WebSocket handshakes to stall for 10 to 30 seconds even after the browser has already loaded the HTML and JavaScript bundle over a warm HTTP/2 connection.
When we diagnosed intermittent admin dashboard hydration delays and chat drawer latency on high-traffic platforms including KortiBundle (kortibundle.com), network telemetry revealed a striking asymmetry: while a fresh TCP + TLS + HTTP/1.1 Upgrade: websocket connection to a shared Anycast edge IP was stalling on ISP middleboxes, requests multiplexed over the browser's already-open HTTP/2 connection completed in under 150ms. Here is how we engineered a Dual-Stack WebSocket + Warm HTTP/2 RPC Failover Architecture that guarantees instant UI responsiveness under hostile ISP conditions.
1. Why `HTTP/2` Survives When `HTTP/1.1` WebSocket Handshakes Stall
When a user visits a modern web application, the browser negotiates TLS 1.3 with ALPN h2 (HTTP/2) and downloads index.html, CSS, and the main JS bundle over a single multiplexed TCP connection. Standard browser WebSocket implementations, however, frequently open a brand-new TCP + TLS socket using ALPN http/1.1 so they can send the classic Connection: Upgrade / Upgrade: websocket headers.
If the user's ISP is throttling new TCP SYN packets or delaying HTTP/1.1 upgrade flows to shared CDN Anycast subnets, two failure modes appear if the frontend blindly awaits ws.onopen:
- Blank Admin Dashboards & Spinners: RPC calls queued behind
await ensureConnected()hang for 15+ seconds waiting for a new TCP socket that the ISP is blackholing. - Connection Pool Starvation from Multi-Socket Racing: Attempting to "fix" slow handshakes by opening 2 or 3 parallel candidate WebSockets simultaneously actually worsens ISP throttling by saturating the browser's 6-connection-per-origin limit.
2. Client-Side Implementation: The 450ms Fast-WS Gate + Warm HTTP/2 Fallback
Instead of blocking UI rendering or RPC execution on a potentially throttled WebSocket handshake, the client transport layer checks whether the socket is already in WebSocket.OPEN state or reaches it within 450ms. If not, it immediately dispatches the identical action payload over a standard fetch('/api/rpc') call, which reuses the browser's warm HTTP/2 connection without opening a new TCP socket:
// TypeScript: Dual-Stack WebSocket + Warm HTTP/2 JSON-RPC Transport
async function waitForFastWs(maxWaitMs = 450): Promise<boolean> {
if (ws && ws.readyState === WebSocket.OPEN) return true;
ensureConnected().catch(() => {}); // Kick off background connection without blocking
const deadline = performance.now() + maxWaitMs;
while (performance.now() < deadline) {
if (ws && ws.readyState === WebSocket.OPEN) return true;
await new Promise((r) => setTimeout(r, 35));
}
return Boolean(ws && ws.readyState === WebSocket.OPEN);
}
async function httpRpc<T = any>(action: string, params: Record<string, any> = {}): Promise<T> {
const res = await fetch('/api/rpc', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Client-Guard': '1',
},
body: JSON.stringify({ action, params, client_id: getClientId() }),
});
if (!res.ok) throw new Error(`RPC HTTP ${res.status}`);
const payload = await res.json();
if (payload.ok === false) throw new Error(payload.detail || 'RPC Error');
return payload.data as T;
}
export async function wsRpc<T = any>(action: string, params: Record<string, any> = {}): Promise<T> {
const wsReady = await waitForFastWs(450);
if (!wsReady) {
return await httpRpc<T>(action, params);
}
// Fast WebSocket path when socket is open...
return await dispatchOverOpenWebSocket<T>(action, params);
}
3. Unified Backend Dispatch in FastAPI
On the FastAPI backend, we expose a lightweight POST /api/rpc endpoint that delegates to the exact same dispatch_ws_rpc(action, params) handler used by the WebSocket loop. We apply the same architecture to streaming LLM responses: if the WebSocket is open within 450ms, tokens stream over WebSocket frames; otherwise, the client falls back transparently to Server-Sent Events (SSE) over POST /api/ollama/chat on the warm HTTP/2 connection.
| Network Condition / Metric | Pure WebSocket-Only Transport | Dual-Stack WS + Warm HTTP/2 Failover |
|---|---|---|
| Healthy Fiber / 5G Connection | 45ms – 90ms RPC latency (WebSocket) | 45ms – 90ms RPC latency (WebSocket) |
| ISP Throttling New TCP / WS Upgrades | 15s – 60s UI hang or timeout failure | 140ms – 280ms via Warm HTTP/2 multiplexing |
| Chat Drawer & Admin Dashboard Open Time | Blocked until WebSocket handshake finishes | 0ms synchronous mount + instant HTTP/2 fetch |
| Backend Code Duplication | N/A | Zero (shared `dispatch_ws_rpc` router) |
Explore related production architectures, live Kenyan legal tools, and enterprise guides:
- High-Throughput WebSocket Binary Streaming & Progressive Canvas Hydration — Rendering 500-page document workspaces in the browser without memory bloat.
- Adaptive Concurrency Limits: Preventing Cascading Failures — Protecting backend worker pools under degraded network and upstream latency.
- KortiBundle Live Production Studio — Experience zero-latency document pagination and Dual-Stack WebSocket + HTTP/2 resilience.