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.
Trình duyệt được thiết kế cho mô hình request–response. 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 đó. 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.
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 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. 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 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();
Nó đơn giản, thân thiện cache, và đi qua mọi corporate proxy. 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.
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 đả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 cải thiện mạnh—event đến ngay khi server có, không chờ poll tick tiếp theo. 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 từng là công cụ chính của app Comet đầu thời (Gmail, Facebook chat khoảng 2010). 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 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. 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
Tự reconnect với Last-Event-ID. Khi connection rớt, browser reconnect và gửi id: header cuối cùng; server có thể replay event bị miss. Bạn không có sẵn điều này với WebSocket thuần—phải tự thiết kế.
Giữ nguyên HTTP semantics. Cookie, auth middleware chuẩn, CDN edge rules, và observability (access log, WAF) hoạt động như mọi GET.
HTTP/2 và HTTP/3 multiplexing. SSE stream chia sẻ một TCP/QUIC connection với REST API; không port thêm, không upgrade dance.
Đơn giản. 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
- Chỉ text — binary payload cần Base64 hoặc kênh riêng.
- Chỉ server→client — hành động client vẫn qua REST hoặc kênh thứ hai.
- 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 buffer
text/event-streamsai. - 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.
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
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 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
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 frame mang opcode + payload; ranh giới message được giữ (khác TCP byte stream).
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
WebSocket mạnh nhưng trần trụi.
| 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 cho biết byte browser đã queue chưa lên wire. 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));
}
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 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 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) gom candidate address: host (LAN local), server-reflexive (public IP qua STUN), relay (qua TURN). ~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 |
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: mỗi peer nối mọi peer khác—ổn ≤4 người; (O(n^2)) connection.
- 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 mix stream—đắt, hiếm hiện nay.
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. Đừ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 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 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 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 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 |
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 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
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 ý. 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 mỗi lần reconnect; đừng giả định socket mở xuyên suốt token TTL.
Scaling and fan-out
Một process Node giữ socket map in-memory—ổn cho prototype.
- Sticky sessions (same client → same worker) for stateful connections, or
- 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 (“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
Network reorder; reconnect duplicate. Gán seq monotonic mỗi room/user; client bỏ qua seq <= lastApplied. Mutation mang idempotency key để frame retry không charge đôi hay duplicate message.
Offline and reconnect UX
User nhận ra khoảng trống.
- Hiện trạng thái disconnected (banner không chặn).
- Fetch snapshot qua REST (
GET /chat/room/42?since=seq). - Tiếp tục live stream (SSE
Last-Event-IDhoặc WS với paramsince). - Merge với CRDT/OT nếu cộng tác.
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 lifecycle connection (open, close code, duration, bytes in/out). Close code 1006 (abnormal) thường là proxy timeout—sửa heartbeat. Correlate trace_id giữa REST và message socket để debug ticket “message không tới”.
Decision guide for principal engineers
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 cho dashboard analytics chỉ đọc — SSE giảm một nửa gánh ops.
- WebRTC cho notification broadcast từ server — không lợi P2P; lãng phí signaling lớn.
- Không strategy reconnect vì “dùng WebSocket rồi” — protocol không cứu bạn.
- Bỏ qua
bufferedAmount— memory tăng im lặng và tab crash dưới tải. - Giả định HTTP/2 sửa long polling — vẫn trả giá request semantics mỗi event.
Closing perspective
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 vẫn là lựa chọn đúng bị dùng thiếu nhất cho server push. 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 và WebTransport là accelerator chuyên dụng, không phải nâng cấp universal.
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.