Real-Time Frontend: Long-Polling, SSE, WebSockets, WebRTC, and WebTransport Compared
A principal-level guide to choosing server push, full-duplex, P2P, and HTTP/3 transport on the frontend—with trade-offs, reconnection, and production scaling.
The browser was built for request–response. {Trình duyệt được thiết kế cho mô hình request–response.} When your product needs live dashboards, collaborative editors, in-app chat, or sub-100ms game state, you are fighting that default. {Khi sản phẩm cần dashboard live, editor cộng tác, chat trong app, hay game state dưới 100ms, bạn đang chống lại mặc định đó.} The five mechanisms covered here—long-polling, Server-Sent Events (SSE), WebSockets, WebRTC, and WebTransport—each sit at a different point on the spectrum of directionality, reliability, latency, and operational cost. {Năm cơ chế ở đây—long-polling, Server-Sent Events (SSE), WebSockets, WebRTC, và WebTransport—nằm ở các điểm khác nhau trên phổ directionality, reliability, latency và chi phí vận hành.}
This post is written for engineers who already know fetch and HTTP status codes; the goal is to explain why each protocol exists, what you must build yourself, and how to choose under production constraints. {Bài viết dành cho engineer đã quen fetch và HTTP status codes; mục tiêu là giải thích tại sao mỗi protocol tồn tại, bạn phải tự build gì, và cách chọn dưới ràng buộc production.}
The need for server push
HTTP/1.1 is fundamentally client-initiated: the server cannot send data until the client opens a connection and asks. {HTTP/1.1 về bản chất do client khởi tạo: server không gửi data cho đến khi client mở connection và hỏi.} Real-time UX therefore requires either simulated push (the client keeps asking) or a persistent channel where the server can write whenever it has something new. {UX real-time vì vậy cần push giả lập (client hỏi liên tục) hoặc kênh persistent nơi server ghi bất cứ khi nào có dữ liệu mới.}
Short polling
Short polling is the naive baseline: the client calls an endpoint every N seconds. {Short polling là baseline đơn giản: client gọi endpoint mỗi N giây.}
async function poll() {
const res = await fetch('/api/notifications?since=' + lastId);
const items = await res.json();
if (items.length) applyUpdates(items);
setTimeout(poll, 3000);
}
poll();
It is simple, cache-friendly, and works through every corporate proxy. {Nó đơn giản, thân thiện cache, và đi qua mọi corporate proxy.} The cost is brutal at scale: most responses are empty, connection setup (TCP + TLS + HTTP) repeats constantly, and latency is bounded below by your interval—3 s polling means up to 3 s stale data. {Chi phí rất nặng khi scale: hầu hết response rỗng, setup connection (TCP + TLS + HTTP) lặp liên tục, và latency bị giới hạn bởi interval—polling 3s nghĩa là data có thể stale tới 3 giây.}
Rule of thumb: short polling is acceptable only for low-frequency, low-stakes updates (e.g. “check for new version” every 60 s). {Quy tắc ngón tay cái: short polling chỉ chấp nhận được cho update tần suất thấp, ít quan trọng (vd. “check version mới” mỗi 60 giây).}
Long polling
Long polling inverts the waste: the client opens a request and the server holds it open until an event arrives or a timeout fires, then the client immediately opens the next one. {Long polling đảo ngược lãng phí: client mở request và server giữ mở đến khi có event hoặc timeout, rồi client mở request tiếp theo ngay.}
async function longPoll() {
try {
const res = await fetch('/api/events/wait?timeout=30', {
signal: AbortSignal.timeout(35000),
});
const event = await res.json();
handleEvent(event);
} catch (err) {
// timeout or network error — backoff before retry
}
longPoll();
}
longPoll();
Latency improves dramatically—events arrive as soon as the server has them, not on the next poll tick. {Latency cải thiện mạnh—event đến ngay khi server có, không chờ poll tick tiếp theo.} But each event still pays a full request cycle; under burst traffic you get connection churn and head-of-line blocking on HTTP/1.1 (one hanging request per tab per resource). {Nhưng mỗi event vẫn trả giá một vòng request; dưới burst traffic bạn gặp connection churn và head-of-line blocking trên HTTP/1.1 (một hanging request mỗi tab mỗi resource).}
| Concern | Short polling | Long polling |
|---|---|---|
| Latency | Up to poll interval | Near real-time on event |
| Server load | High (constant empty hits) | Medium (held connections) |
| Connection count | Bursty | One per client, always open |
| HTTP/2 benefit | Marginal | Multiplexing helps, but still request-per-event |
| Client complexity | Trivial | Retry/backoff required |
Long polling was the workhorse of early Comet-era apps (Gmail, Facebook chat circa 2010). {Long polling từng là công cụ chính của app Comet đầu thời (Gmail, Facebook chat khoảng 2010).} It remains a valid fallback when WebSockets are blocked, but you inherit all the HTTP overhead of reopening connections. {Nó vẫn là fallback hợp lệ khi WebSocket bị chặn, nhưng bạn kế thừa toàn bộ HTTP overhead khi mở lại connection.}
Server-Sent Events (SSE)
SSE is the browser-native answer to unidirectional server→client push over ordinary HTTP. {SSE là câu trả lời native của browser cho push một chiều server→client trên HTTP thông thường.} You subscribe with EventSource; the server responds with Content-Type: text/event-stream and streams UTF-8 text frames. {Bạn subscribe bằng EventSource; server trả Content-Type: text/event-stream và stream UTF-8 text frames.}
<script>
const source = new EventSource('/api/stream', { withCredentials: true });
source.addEventListener('price-update', (e) => {
const data = JSON.parse(e.data);
renderTicker(data);
});
source.onerror = () => {
// browser auto-reconnects; check source.readyState
};
</script>
Server side (Node/Express sketch):
app.get('/api/stream', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.flushHeaders();
const id = subscribe(req.user.id, (payload) => {
res.write(`event: price-update\n`);
res.write(`id: ${payload.seq}\n`);
res.write(`data: ${JSON.stringify(payload)}\n\n`);
});
req.on('close', () => unsubscribe(id));
});
Why SSE wins for many “live feed” products
Automatic reconnection with Last-Event-ID. {Tự reconnect với Last-Event-ID.} When the connection drops, the browser reconnects and sends the last received id: header; your server can replay missed events. {Khi connection rớt, browser reconnect và gửi id: header cuối cùng; server có thể replay event bị miss.} You do not get this for free with raw WebSockets—you must design it. {Bạn không có sẵn điều này với WebSocket thuần—phải tự thiết kế.}
HTTP semantics preserved. {Giữ nguyên HTTP semantics.} Cookies, standard auth middleware, CDN edge rules, and observability (access logs, WAF) work as they do for any GET. {Cookie, auth middleware chuẩn, CDN edge rules, và observability (access log, WAF) hoạt động như mọi GET.}
HTTP/2 and HTTP/3 multiplexing. {HTTP/2 và HTTP/3 multiplexing.} An SSE stream shares one TCP/QUIC connection with your REST API; no extra port, no upgrade dance. {SSE stream chia sẻ một TCP/QUIC connection với REST API; không port thêm, không upgrade dance.}
Simplicity. {Đơn giản.} One direction means no client-send framing, no subprotocol negotiation, no custom heartbeat protocol (though sending comment lines : keepalive\n\n every ~15–30 s prevents proxy timeouts). {Một chiều nghĩa là không framing client-send, không negotiate subprotocol, không heartbeat protocol tùy biến (dù gửi comment : keepalive\n\n mỗi ~15–30 giây tránh proxy timeout).}
SSE limits you must accept
- Text only — binary payloads require Base64 or a separate channel. {Chỉ text — binary payload cần Base64 hoặc kênh riêng.}
- Server→client only — client actions still go through REST or a second channel. {Chỉ server→client — hành động client vẫn qua REST hoặc kênh thứ hai.}
- Per-browser connection limits — historically ~6 concurrent HTTP/1.1 connections per origin; HTTP/2 mitigates via one connection, but some proxies buffer
text/event-streamincorrectly. {Giới hạn connection mỗi browser — lịch sử ~6 HTTP/1.1 concurrent mỗi origin; HTTP/2 giảm nhờ một connection, nhưng một số proxy buffertext/event-streamsai.} - No built-in backpressure signal to the client — if the client JS thread stalls, events queue in the browser; design idempotent handlers. {Không có backpressure signal tới client — nếu JS thread client stall, event xếp hàng trong browser; thiết kế handler idempotent.}
When to choose SSE: live notifications, stock tickers, build/deploy logs, feature-flag streams, read-only collaborative presence indicators—any case where the client mostly listens. {Khi chọn SSE: notification live, ticker, log build/deploy, stream feature flag, presence chỉ đọc—mọi case client chủ yếu lắng nghe.}
WebSockets
WebSockets provide a full-duplex, persistent, framed channel initiated by an HTTP Upgrade handshake. {WebSocket cung cấp kênh full-duplex, persistent, có framing khởi tạo bằng HTTP Upgrade handshake.}
GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Server responds 101 Switching Protocols; thereafter both sides send frames (text, binary, ping, pong, close) over the same TCP connection. {Server trả 101 Switching Protocols; sau đó hai bên gửi frame (text, binary, ping, pong, close) trên cùng TCP connection.}
const ws = new WebSocket('wss://example.com/chat', ['json.v1']);
ws.onopen = () => {
ws.send(JSON.stringify({ type: 'join', room: 'general' }));
};
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
dispatch(msg);
};
ws.onclose = (event) => {
scheduleReconnect(event.code, event.reason);
};
Subprotocols and framing
The second argument to WebSocket is a subprotocol list (e.g. graphql-transport-ws, json.v1)—a contract for message shape, not encryption. {Tham số thứ hai của WebSocket là danh sách subprotocol (vd. graphql-transport-ws, json.v1)—hợp đồng cho message shape, không phải mã hóa.} WebSocket frames carry opcode + payload; message boundaries are preserved (unlike TCP byte streams). {WebSocket frame mang opcode + payload; ranh giới message được giữ (khác TCP byte stream).}
Binary frames matter for protobuf, MessagePack, or compressed game state—SSE cannot do this natively. {Binary frame quan trọng cho protobuf, MessagePack, hay game state nén—SSE không làm native.}
What the platform does not give you
WebSockets are powerful but bare. {WebSocket mạnh nhưng trần trụi.} There is no standard for:
| Missing primitive | What you build |
|---|---|
| Reconnection | Exponential backoff + jitter; resume token or snapshot sync |
| Heartbeat | App-level ping/pong or rely on WS protocol ping (server must implement) |
| Backpressure | Check bufferedAmount; pause send() or drop/coalesce |
| Auth refresh | Re-authenticate on reconnect; short-lived tokens in first frame |
| Message ordering | Sequence numbers + idempotent handlers if multiple tabs/workers |
| Multiplexing | One socket per domain usually; multiplex logical channels in app layer |
A minimal production reconnect wrapper:
function connectSocket(url: string, getToken: () => Promise<string>) {
let ws: WebSocket | null = null;
let attempt = 0;
const queue: string[] = [];
async function open() {
const token = await getToken();
ws = new WebSocket(`${url}?token=${token}`);
ws.onopen = () => {
attempt = 0;
while (queue.length && ws!.readyState === WebSocket.OPEN) {
ws!.send(queue.shift()!);
}
};
ws.onclose = () => {
const delay = Math.min(30_000, 1000 * 2 ** attempt) + Math.random() * 500;
attempt++;
setTimeout(open, delay);
};
}
return {
send(data: string) {
if (ws?.readyState === WebSocket.OPEN) ws.send(data);
else queue.push(data);
},
start: open,
};
}
Backpressure in practice
WebSocket.bufferedAmount exposes bytes queued by the browser that have not hit the wire. {WebSocket.bufferedAmount cho biết byte browser đã queue chưa lên wire.} If it grows monotonically while you call send(), you are producing faster than the network (or the peer) can consume—coalesce updates (latest cursor position wins), drop non-critical frames, or apply flow control at the protocol level. {Nếu nó tăng liên tục khi bạn send(), bạn produce nhanh hơn network (hoặc peer) consume—gộp update (cursor position mới nhất thắng), drop frame không quan trọng, hoặc flow control ở tầng protocol.}
function safeSend(ws, payload) {
if (ws.bufferedAmount > 64 * 1024) {
pendingPayload = payload; // coalesce to latest
return;
}
ws.send(JSON.stringify(payload));
}
When to choose WebSockets: chat, multiplayer game input, collaborative CRDT sync, trading terminals—any bidirectional, low-latency, high-frequency client↔server traffic. {Khi chọn WebSocket: chat, input game multiplayer, sync CRDT cộng tác, terminal trading—mọi traffic client↔server hai chiều, latency thấp, tần suất cao.}
WebRTC
WebRTC is not a replacement for WebSockets—it is a peer-to-peer transport for media (audio/video via RTCPeerConnection) and arbitrary data (RTCDataChannel). {WebRTC không thay WebSocket—nó là transport peer-to-peer cho media (audio/video qua RTCPeerConnection) và data tùy ý (RTCDataChannel).} Signaling (SDP offer/answer, ICE candidates) still flows through your server—typically WebSocket or HTTP—but media/data may flow directly between browsers. {Signaling (SDP offer/answer, ICE candidate) vẫn qua server—thường WebSocket hoặc HTTP—nhưng media/data có thể đi trực tiếp giữa browser.}
const pc = new RTCPeerConnection({
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }],
});
const channel = pc.createDataChannel('game', { ordered: true });
channel.onopen = () => channel.send(JSON.stringify({ type: 'input', keys: [] }));
channel.onmessage = (e) => applyRemoteState(JSON.parse(e.data));
// signaling via your WebSocket
signal.on('candidate', (c) => pc.addIceCandidate(c));
const offer = await pc.createOffer();
await pc.setLocalDescription(offer);
signal.send({ type: 'offer', sdp: offer });
ICE, STUN, and TURN
ICE (Interactive Connectivity Establishment) gathers candidate addresses: host (local LAN), server-reflexive (public IP via STUN), relay (via TURN). {ICE (Interactive Connectivity Establishment) gom candidate address: host (LAN local), server-reflexive (public IP qua STUN), relay (qua TURN).} ~15–20% of sessions fail without TURN in enterprise NAT/firewall environments—budget for TURN infrastructure. {~15–20% session fail nếu không có TURN trong môi trường NAT/firewall doanh nghiệp—cần budget TURN infrastructure.}
Data channels vs media tracks
| Channel | Ordered | Reliable | Typical use |
|---|---|---|---|
| Ordered + reliable (default) | Yes | Yes | File transfer, game state sync |
| Unordered + maxRetransmits: 0 | No | No | Voice-like positional updates |
Media (addTrack) | N/A | N/A | Video/audio with jitter buffers |
Unreliable unordered data channels mirror UDP semantics—ideal when freshness beats completeness (FPS aim vectors, live cursor at 60 Hz). {Data channel unreliable unordered giống UDP—lý tưởng khi freshness quan trọng hơn completeness (vector aim FPS, cursor live 60 Hz).}
Topology: mesh, SFU, MCU
- Mesh: each peer connects to every other peer—works for ≤4 participants; (O(n^2)) connections. {Mesh: mỗi peer nối mọi peer khác—ổn ≤4 người; (O(n^2)) connection.}
- SFU (Selective Forwarding Unit): peers send one upstream to server; server forwards selective streams—standard for group video (Zoom-like). {SFU: peer gửi một upstream lên server; server forward stream có chọn—chuẩn cho video nhóm (kiểu Zoom).}
- MCU: server mixes streams—expensive, rare now. {MCU: server mix stream—đắt, hiếm hiện nay.}
When to choose WebRTC: video/voice calls, P2P file transfer, ultra-low-latency multiplayer where server relay adds unacceptable RTT, LAN-adjacent experiences. {Khi chọn WebRTC: gọi video/voice, transfer file P2P, multiplayer latency cực thấp khi relay server thêm RTT không chấp nhận được, trải nghiệm gần LAN.} Do not choose it for simple dashboard push—operational complexity is an order of magnitude higher. {Đừng chọn cho dashboard push đơn giản—độ phức tạp vận hành cao hơn một bậc.}
WebTransport (HTTP/3 / QUIC)
WebTransport is a modern API exposing QUIC-based communication: multiple bidirectional streams, unidirectional streams, and unreliable datagrams—all over HTTP/3. {WebTransport là API hiện đại expose giao tiếp dựa QUIC: nhiều stream hai chiều, stream một chiều, và datagram unreliable—tất cả trên HTTP/3.}
const transport = new WebTransport('https://example.com/wt');
await transport.ready;
const stream = await transport.createBidirectionalStream();
const writer = stream.writable.getWriter();
const reader = stream.readable.getReader();
await writer.write(new TextEncoder().encode('hello'));
const { value, done } = await reader.read();
// unreliable, unordered — like UDP within QUIC
transport.datagrams.writable.getWriter().write(new Uint8Array([1, 2, 3]));
Why QUIC changes the trade-off
TCP + TLS + HTTP/2 suffers head-of-line blocking at the byte stream level: one lost packet stalls every multiplexed stream on that connection. {TCP + TLS + HTTP/2 gặp head-of-line blocking ở tầng byte stream: một packet mất làm stall mọi stream multiplex trên connection đó.} QUIC encrypts per-packet and multiplexes independent streams—loss on one stream does not block unrelated streams. {QUIC mã hóa từng packet và multiplex stream độc lập—mất trên một stream không block stream khác.}
WebTransport datagrams give browsers UDP-like behavior with QUIC’s encryption and congestion control, without opening raw UDP sockets (which browsers forbid). {WebTransport datagram cho browser hành vi giống UDP với encryption và congestion control của QUIC, không cần mở raw UDP socket (browser cấm).}
WebTransport vs WebSockets (2025–2026 landscape)
| Dimension | WebSocket | WebTransport |
|---|---|---|
| Wire protocol | TCP (often HTTP/1.1 upgrade) | HTTP/3 / QUIC |
| Multiplexing | Single channel; app-layer demux | Native many streams + datagrams |
| Unreliable mode | No (app must drop stale msgs) | First-class datagrams |
| Head-of-line blocking | TCP-level affects all traffic | Per-stream isolation |
| Browser support | Universal | Chromium stable; Firefox/Safari catching up—verify caniuse before betting product |
| Infrastructure | Mature (nginx, AWS ALB, Cloudflare) | Growing; needs HTTP/3-capable edge |
| Fallback | — | Often pair with WebSocket for unsupported clients |
As of 2026, treat WebTransport as opt-in for latency-sensitive, multiplex-heavy workloads where you control the edge (game backends, custom CDNs), not as the default replacement for every WebSocket. {Tính đến 2026, coi WebTransport là opt-in cho workload nhạy latency, multiplex nặng khi bạn kiểm soát edge (game backend, CDN tùy biến), không phải thay mặc định mọi WebSocket.}
async function createRealtimeTransport() {
if ('WebTransport' in window && await supportsHttp3()) {
return new WebTransportTransport('/wt');
}
return new WebSocketTransport('/ws');
}
Server implementations exist in Node (@fails-components/webtransport, h3-webtransport), Cloudflare Workers, and Caddy—all require TLS 1.3 and HTTP/3 enabled. {Server implementation có trên Node (@fails-components/webtransport, h3-webtransport), Cloudflare Workers, và Caddy—đều cần TLS 1.3 và HTTP/3 bật.}
Protocol comparison
| Mechanism | Direction | Reliability / ordering | Transport | Typical latency | Reconnection | Best fit |
|---|---|---|---|---|---|---|
| Short polling | Client pull | Per HTTP request | HTTP | Interval-bound | Manual | Rare checks |
| Long polling | Simulated push | Per HTTP request | HTTP | Low on event | Manual | Legacy / restrictive proxies |
| SSE | Server→client | Ordered, reliable | HTTP | Low | Built-in + Last-Event-ID | Live feeds, logs, notifications |
| WebSocket | Full-duplex | Ordered, reliable frames | TCP (WS) | Very low | DIY | Chat, sync, gaming vs server |
| WebRTC DataChannel | Peer↔peer | Configurable | UDP (SRTP/ SCTP) | Lowest LAN/WAN P2P | DIY + ICE restart | AV, P2P data, mesh/SFU |
| WebTransport | Full-duplex + datagrams | Streams reliable; datagrams not | QUIC/HTTP/3 | Very low | DIY (no standard yet) | Multiplexed games, mixed reliability |
Production concerns
Authentication and authorization
Cookies work on SSE (withCredentials: true) and on the WebSocket upgrade request—but many JWT-in-header APIs break because browser WebSocket cannot set arbitrary headers. {Cookie hoạt động trên SSE (withCredentials: true) và upgrade request WebSocket—nhưng nhiều API JWT-in-header hỏng vì WebSocket browser không set header tùy ý.} Common patterns: short-lived token in query string (leaks via logs—rotate quickly), cookie-based session, or Sec-WebSocket-Protocol token (hacky). {Pattern phổ biến: token ngắn hạn trong query string (lộ qua log—rotate nhanh), session cookie, hoặc token Sec-WebSocket-Protocol (hacky).} Re-auth on every reconnect; never assume the socket stays open across token TTL. {Re-auth mỗi lần reconnect; đừng giả định socket mở xuyên suốt token TTL.}
Scaling and fan-out
A single Node process holds in-memory socket maps—fine for prototypes. {Một process Node giữ socket map in-memory—ổn cho prototype.} Production requires:
- Sticky sessions (same client → same worker) for stateful connections, or
- Pub/sub backbone (Redis, NATS, Kafka) where each worker subscribes and pushes to its local sockets. {Sticky session (cùng client → cùng worker) cho connection stateful, hoặc pub/sub backbone (Redis, NATS, Kafka) mỗi worker subscribe và push tới socket local.}
Presence (“who is online”) must be eventually consistent—heartbeats with TTL keys in Redis beat scanning socket lists. {Presence (“ai online”) phải eventually consistent—heartbeat với TTL key trong Redis tốt hơn quét danh sách socket.}
Message ordering and idempotency
Networks reorder; reconnects duplicate. {Network reorder; reconnect duplicate.} Assign monotonic seq per room/user; clients ignore seq <= lastApplied. {Gán seq monotonic mỗi room/user; client bỏ qua seq <= lastApplied.} Mutations carry idempotency keys so retried frames do not double-charge or duplicate messages. {Mutation mang idempotency key để frame retry không charge đôi hay duplicate message.}
Offline and reconnect UX
Users notice gaps. {User nhận ra khoảng trống.} On reconnect:
- Show disconnected state (non-blocking banner). {Hiện trạng thái disconnected (banner không chặn).}
- Fetch snapshot via REST (
GET /chat/room/42?since=seq). {Fetch snapshot qua REST (GET /chat/room/42?since=seq).} - Resume live stream (SSE
Last-Event-IDor WS withsinceparam). {Tiếp tục live stream (SSELast-Event-IDhoặc WS với paramsince).} - Merge with CRDT/OT if collaborative. {Merge với CRDT/OT nếu cộng tác.}
Exponential backoff with full jitter prevents thundering herd when your region comes back online. {Exponential backoff với full jitter tránh thundering herd khi region online lại.}
function backoffDelay(attempt) {
const cap = 30_000;
const base = Math.min(cap, 1000 * 2 ** attempt);
return Math.random() * base;
}
Observability
Log connection lifecycle (open, close code, duration, bytes in/out). {Log lifecycle connection (open, close code, duration, bytes in/out).} Close code 1006 (abnormal) usually means proxy timeout—fix heartbeats. {Close code 1006 (abnormal) thường là proxy timeout—sửa heartbeat.} Correlate trace_id across REST and socket messages for debugging “message never arrived” tickets. {Correlate trace_id giữa REST và message socket để debug ticket “message không tới”.}
Decision guide for principal engineers
Use this flow—not as gospel, but to frame architecture reviews. {Dùng flow này—không phải chân lý—để khung architecture review.}
Need server → client only, text/JSON, standard HTTP stack?
└─ YES → SSE (default for feeds)
└─ NO ↓
Need bidirectional client ↔ server over the internet, broad browser support?
└─ YES → WebSocket (+ DIY reconnect, heartbeat, backpressure)
└─ NO ↓
Need peer ↔ peer media or lowest RTT data between users?
└─ YES → WebRTC (+ TURN budget, signaling service, SFU for groups)
└─ NO ↓
Need multiple reliability modes + stream multiplex on HTTP/3, you control edge?
└─ YES → WebTransport with WebSocket fallback
└─ NO ↓
Corporate proxies block everything except vanilla HTTP?
└─ Long polling (last resort)
Anti-patterns worth rejecting in review
- WebSocket for a read-only analytics dashboard — SSE is half the ops burden. {WebSocket cho dashboard analytics chỉ đọc — SSE giảm một nửa gánh ops.}
- WebRTC for server-broadcast notifications — no P2P benefit; massive signaling waste. {WebRTC cho notification broadcast từ server — không lợi P2P; lãng phí signaling lớn.}
- No reconnect strategy because “we’ll use WebSocket” — the protocol does not save you. {Không strategy reconnect vì “dùng WebSocket rồi” — protocol không cứu bạn.}
- Ignoring
bufferedAmount— silent memory growth and tab crashes under load. {Bỏ quabufferedAmount— memory tăng im lặng và tab crash dưới tải.} - Assuming HTTP/2 fixes long polling — you still pay per-event request semantics. {Giả định HTTP/2 sửa long polling — vẫn trả giá request semantics mỗi event.}
Closing perspective
Real-time frontend architecture is less about picking the newest API and more about matching directionality, reliability, and operational surface to your product’s failure modes. {Kiến trúc real-time frontend ít về chọn API mới nhất mà về khớp directionality, reliability, và operational surface với failure mode sản phẩm.} SSE remains the most underused correct choice for server push. {SSE vẫn là lựa chọn đúng bị dùng thiếu nhất cho server push.} WebSockets remain the default full-duplex workhorse—provided you budget engineering for everything TCP already gave you for free in HTTP. {WebSocket vẫn là workhorse full-duplex mặc định—nếu bạn budget engineering cho mọi thứ TCP/HTTP từng cho miễn phí.} WebRTC and WebTransport are specialized accelerators, not universal upgrades. {WebRTC và WebTransport là accelerator chuyên dụng, không phải nâng cấp universal.}
Ship the simplest channel that meets latency and direction requirements; measure close codes, reconnect rates, and p99 delivery latency before optimizing protocols. {Ship kênh đơn giản nhất đáp latency và hướng yêu cầu; đo close code, tỷ lệ reconnect, và p99 delivery latency trước khi tối ưu protocol.}