jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

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).}

ConcernShort pollingLong polling
LatencyUp to poll intervalNear real-time on event
Server loadHigh (constant empty hits)Medium (held connections)
Connection countBurstyOne per client, always open
HTTP/2 benefitMarginalMultiplexing helps, but still request-per-event
Client complexityTrivialRetry/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-stream incorrectly. {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-stream sai.}
  • 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 primitiveWhat you build
ReconnectionExponential backoff + jitter; resume token or snapshot sync
HeartbeatApp-level ping/pong or rely on WS protocol ping (server must implement)
BackpressureCheck bufferedAmount; pause send() or drop/coalesce
Auth refreshRe-authenticate on reconnect; short-lived tokens in first frame
Message orderingSequence numbers + idempotent handlers if multiple tabs/workers
MultiplexingOne 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

ChannelOrderedReliableTypical use
Ordered + reliable (default)YesYesFile transfer, game state sync
Unordered + maxRetransmits: 0NoNoVoice-like positional updates
Media (addTrack)N/AN/AVideo/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)

DimensionWebSocketWebTransport
Wire protocolTCP (often HTTP/1.1 upgrade)HTTP/3 / QUIC
MultiplexingSingle channel; app-layer demuxNative many streams + datagrams
Unreliable modeNo (app must drop stale msgs)First-class datagrams
Head-of-line blockingTCP-level affects all trafficPer-stream isolation
Browser supportUniversalChromium stable; Firefox/Safari catching up—verify caniuse before betting product
InfrastructureMature (nginx, AWS ALB, Cloudflare)Growing; needs HTTP/3-capable edge
FallbackOften 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

MechanismDirectionReliability / orderingTransportTypical latencyReconnectionBest fit
Short pollingClient pullPer HTTP requestHTTPInterval-boundManualRare checks
Long pollingSimulated pushPer HTTP requestHTTPLow on eventManualLegacy / restrictive proxies
SSEServer→clientOrdered, reliableHTTPLowBuilt-in + Last-Event-IDLive feeds, logs, notifications
WebSocketFull-duplexOrdered, reliable framesTCP (WS)Very lowDIYChat, sync, gaming vs server
WebRTC DataChannelPeer↔peerConfigurableUDP (SRTP/ SCTP)Lowest LAN/WAN P2PDIY + ICE restartAV, P2P data, mesh/SFU
WebTransportFull-duplex + datagramsStreams reliable; datagrams notQUIC/HTTP/3Very lowDIY (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:

  1. Sticky sessions (same client → same worker) for stateful connections, or
  2. 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:

  1. Show disconnected state (non-blocking banner). {Hiện trạng thái disconnected (banner không chặn).}
  2. Fetch snapshot via REST (GET /chat/room/42?since=seq). {Fetch snapshot qua REST (GET /chat/room/42?since=seq).}
  3. Resume live stream (SSE Last-Event-ID or WS with since param). {Tiếp tục live stream (SSE Last-Event-ID hoặc WS với param since).}
  4. 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ỏ qua bufferedAmount — 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.}