Polling, Long polling, SSE và WebSocket là gì? Phân biệt 4 cách làm tính năng realtime
Bài này nối tiếp Tập 1: Callback, Webhook và Polling. Nếu chưa đọc, bạn nên lướt qua phần ví dụ tiệm sửa xe trước.
Ở tập trước, hai server nói chuyện với nhau rất dễ: cổng thanh toán xong việc thì gọi thẳng vào URL của bạn. Đó là callback. Còn polling chỉ là phương án dự phòng.
Giờ đổi bối cảnh. Bạn làm tính năng thông báo cho một web app: có người bình luận bài viết của user, chuông thông báo phải sáng lên ngay. Bên có dữ liệu mới là server của bạn, còn bên cần nhận là trình duyệt của user. Server có thể "callback" xuống trình duyệt được không?
Không được. Và chính câu trả lời "không" này sinh ra cả bốn thuật ngữ của bài hôm nay.
Vấn đề gốc: server không gọi được trình duyệt
Callback cần một địa chỉ để gọi tới. Server có URL công khai, nhưng trình duyệt thì không: nó nằm sau router wifi, sau mạng 4G của nhà mạng, không có địa chỉ nào để server gửi request tới.
Thêm nữa, HTTP chạy theo kiểu request–response: client hỏi, server trả lời. Server chỉ "nói" được khi đang có một request của client chờ trả lời. Không có request nào đang mở thì server không có cách nào lên tiếng.
Quay lại ví dụ tiệm sửa xe: lần này bạn không có số điện thoại cố định. Bạn dùng điện thoại công cộng, gọi đi được nhưng không ai gọi tới được. Tiệm không thể báo cho bạn. Mọi cuộc gọi đều phải do bạn bắt đầu.
Vậy câu hỏi thật sự là: khi chỉ client mới mở được kết nối, làm sao để server gửi dữ liệu mới xuống càng nhanh càng tốt? Bốn thuật ngữ trong bài là bốn câu trả lời, mỗi cái sinh ra để vá giới hạn của cái trước.
flowchart LR
A[Polling] -- "Tốn request vô ích" --> B[Long polling]
B -- "Phải mở lại kết nối<br/>sau mỗi tin" --> C[SSE]
C -- "Chỉ đẩy được<br/>một chiều" --> D[WebSocket]
Hình 1: Mạch tiến hoá của bài. Nhãn trên mỗi mũi tên là giới hạn của cách bên trái, cũng là lý do cách bên phải ra đời.
Alt text: Sơ đồ bốn bước tiến hoá từ polling sang long polling, SSE và WebSocket, mỗi bước ghi giới hạn của cách trước.
1. Polling: cứ hỏi định kỳ
Polling là client gửi request định kỳ để hỏi server "có gì mới không?", dù có hay không thì server cũng trả lời ngay.
Ví dụ đời thường: cứ 5 phút bạn gọi điện cho tiệm, hỏi "xe xong chưa?", nghe trả lời rồi cúp máy.
sequenceDiagram
participant B as Trình duyệt
participant S as Server
B->>S: Có gì mới không?
S-->>B: Không có
B->>S: Có gì mới không?
S-->>B: Không có
Note over S: Có thông báo mới
B->>S: Có gì mới không?
S-->>B: Có 1 thông báo
Hình 2: Polling. Để ý thông báo đã có từ trước, nhưng trình duyệt chỉ biết ở lần hỏi kế tiếp.
Alt text: Sơ đồ tuần tự polling, trình duyệt hỏi server lặp lại, phần lớn câu trả lời là không có gì mới.
// Client: 5 giây hỏi một lần
setInterval(async () => {
const res = await fetch(`/api/notifications?since=${lastId}`);
const items = await res.json();
if (items.length) render(items);
}, 5000);
Ưu điểm: cực kỳ đơn giản, chỉ là API bình thường, không cần gì đặc biệt ở server.
Giới hạn: bạn phải chọn giữa nhanh và rẻ. Hỏi 5 giây một lần thì thông báo có thể trễ gần 5 giây. Hỏi mỗi giây thì 10.000 user online tạo ra 10.000 request mỗi giây, mà gần như tất cả đều nhận câu trả lời "không có gì".
Vấn đề là: phần lớn request là vô ích. Có cách nào chỉ trả lời khi thật sự có tin?
2. Long polling: hỏi rồi giữ máy chờ
Long polling là client gửi request, nhưng server không trả lời ngay mà giữ request đó lại cho tới khi có dữ liệu mới (hoặc hết thời gian chờ), rồi client lập tức gửi request tiếp theo.
Ví dụ đời thường: bạn gọi cho tiệm, thợ bảo "anh cứ giữ máy, có gì em báo liền". Khi có tin, thợ báo xong thì cúp máy, và bạn gọi lại ngay để tiếp tục chờ. Nếu giữ máy quá lâu mà không có gì, thợ cũng cúp, và bạn lại gọi lại.
sequenceDiagram
participant B as Trình duyệt
participant S as Server
B->>S: Có gì mới không?
Note over S: Giữ request, chưa trả lời...
Note over S: Có thông báo mới
S-->>B: Có 1 thông báo
B->>S: Có gì mới không? (gửi lại ngay)
Note over S: Giữ request...
Hình 3: Long polling. Server trả lời ngay khi có tin, không phải đợi tới lượt hỏi kế tiếp.
Alt text: Sơ đồ tuần tự long polling, server giữ request cho tới khi có thông báo mới, trình duyệt gửi lại request ngay sau khi nhận.
// Server: giữ response lại, có tin mới thì trả
const waiting = [];
app.get('/api/notifications/wait', (req, res) => {
const timer = setTimeout(() => res.status(204).end(), 30000); // hết 30s → trả rỗng
waiting.push({ res, timer });
});
function notifyAll(message) {
while (waiting.length) {
const { res, timer } = waiting.pop();
clearTimeout(timer);
res.json([message]);
}
}
Phía client là một vòng lặp: gửi request, nhận kết quả (hoặc 204), rồi gửi lại ngay.
Ưu điểm: gần như không có request vô ích, thông báo đến gần như tức thì, và vẫn chỉ là HTTP bình thường nên chạy được ở mọi nơi.
Giới hạn: sau mỗi tin nhắn, kết nối kết thúc và client phải mở request mới, kèm đủ header, cookie, xác thực. Nếu tin đến dồn dập (giá chứng khoán đổi liên tục), client cứ đóng–mở liên tục, và tin đến trong khoảng giữa hai request phải được server lưu tạm để không bị mất.
Vấn đề là: nếu đã giữ được kết nối, sao không giữ luôn và gửi nhiều tin qua cùng một kết nối?
3. SSE: giữ kết nối, server cứ thế đẩy
SSE (Server-Sent Events) là client mở một request HTTP, server không bao giờ "trả lời xong" mà giữ kết nối mở và liên tục đẩy dữ liệu mới xuống theo một định dạng chuẩn.
Ví dụ đời thường: bạn gọi một cuộc, thợ bật loa ngoài và cập nhật liên tục: "đang tháo máy... đã thay nhông xích... xong rồi". Bạn chỉ nghe. Muốn dặn thợ điều gì, bạn phải nhắn tin qua kênh khác.
sequenceDiagram
participant B as Trình duyệt
participant S as Server
B->>S: GET /stream (mở một lần)
S-->>B: Sự kiện 1
S-->>B: Sự kiện 2
S-->>B: Sự kiện 3
Note over B,S: Kết nối vẫn mở...
B->>S: POST /comments (gửi lên dùng request riêng)
Hình 4: SSE. Một kết nối, nhiều tin, nhưng chỉ chảy một chiều từ server xuống. Muốn gửi lên thì vẫn dùng request HTTP thường.
Alt text: Sơ đồ tuần tự SSE, trình duyệt mở một kết nối và server đẩy nhiều sự kiện qua kết nối đó.
// Server (Express)
app.get('/api/stream', (req, res) => {
res.set({ 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache' });
res.flushHeaders();
const send = (msg) => res.write(`id: ${msg.id}\ndata: ${JSON.stringify(msg)}\n\n`);
subscribers.add(send);
req.on('close', () => subscribers.delete(send)); // client đóng tab → dọn dẹp
});
// Client: trình duyệt có sẵn EventSource
const stream = new EventSource('/api/stream');
stream.onmessage = (e) => render(JSON.parse(e.data));
Một điểm hay của SSE: nếu mất kết nối, EventSource tự động kết nối lại, và gửi kèm header Last-Event-ID (lấy từ trường id của tin cuối cùng) để server biết gửi tiếp từ đâu. Bạn có sẵn cơ chế chống mất tin mà không phải tự viết.
Nếu bạn từng thấy câu trả lời của chatbot AI hiện ra từng chữ, khả năng cao đó là SSE. Các API như OpenAI hay Anthropic dùng SSE để stream câu trả lời: client hỏi một lần, server đẩy từng đoạn chữ xuống.
Ưu điểm: vẫn là HTTP nên dễ đi qua proxy, firewall; có tự kết nối lại; code đơn giản.
Giới hạn: chỉ một chiều, và chỉ gửi được dữ liệu dạng text. Với HTTP/1.1, trình duyệt giới hạn khoảng 6 kết nối đồng thời tới cùng một domain, nên mở nhiều tab dùng SSE có thể "ăn" hết chỗ. HTTP/2 giải quyết được việc này vì nhiều luồng dùng chung một kết nối.
Vấn đề là: với ứng dụng chat hay game, client cũng gửi lên liên tục. Mỗi lần gửi lại tạo một request HTTP mới thì tốn kém và chậm. Cần một kênh mà cả hai bên cùng nói được.
4. WebSocket: cuộc gọi hai chiều
WebSocket là một kết nối được giữ mở lâu dài, qua đó cả client và server đều có thể gửi dữ liệu cho nhau bất cứ lúc nào.
Ví dụ đời thường: một cuộc gọi bình thường, cả hai cùng nói, cùng nghe, không ai phải chờ ai hỏi.
WebSocket bắt đầu bằng một request HTTP có header Upgrade: websocket. Server đồng ý bằng mã 101 Switching Protocols, và từ đó kết nối không còn là HTTP nữa mà chuyển sang giao thức WebSocket: gửi các "frame" nhỏ, không cần header lặp lại mỗi tin.
sequenceDiagram
participant B as Trình duyệt
participant S as Server
B->>S: GET /chat (Upgrade: websocket)
S-->>B: 101 Switching Protocols
Note over B,S: Từ đây không còn là HTTP
B->>S: "Chào mọi người"
S->>B: "An vừa vào phòng"
S->>B: "An: Chào bạn"
B->>S: "Đang gõ..."
Hình 5: WebSocket. Sau bước bắt tay, mũi tên đi theo cả hai chiều, và server gửi xuống không cần ai hỏi.
Alt text: Sơ đồ tuần tự WebSocket, bắt tay bằng HTTP Upgrade rồi trình duyệt và server gửi tin nhắn qua lại hai chiều.
// Server: thư viện ws
const { WebSocketServer, WebSocket } = require('ws');
const wss = new WebSocketServer({ port: 8080 });
wss.on('connection', (socket) => {
socket.on('message', (raw) => {
for (const client of wss.clients) { // phát tin cho mọi người trong phòng
if (client.readyState === WebSocket.OPEN) client.send(raw.toString());
}
});
});
// Client
const socket = new WebSocket('ws://localhost:8080');
socket.onmessage = (e) => render(e.data);
sendButton.onclick = () => socket.send(input.value);
Ưu điểm: hai chiều, độ trễ thấp nhất, overhead mỗi tin rất nhỏ, gửi được cả dữ liệu nhị phân.
Giới hạn: không còn là HTTP nên mất nhiều thứ "miễn phí": không tự kết nối lại (phải tự viết hoặc dùng thư viện như Socket.IO), không có cơ chế chống mất tin sẵn có, một số proxy hoặc firewall doanh nghiệp có thể chặn.
Cái giá chung: giữ kết nối thì phải nuôi kết nối
Long polling, SSE và WebSocket đều có một cái giá mà polling không có: server phải giữ hàng nghìn kết nối mở cùng lúc. Điều này kéo theo mấy hệ quả cho backend:
- Mỗi kết nối chiếm bộ nhớ. Server theo mô hình một thread cho mỗi request sẽ cạn thread rất nhanh; mô hình bất đồng bộ (Node.js, Go, Java virtual thread, Netty...) phù hợp hơn.
- Khi chạy nhiều instance sau load balancer, user A kết nối vào instance 1, user B ở instance 2. Muốn gửi tin từ A sang B, các instance cần một kênh chung để báo cho nhau, thường là Redis Pub/Sub hoặc message broker.
- Load balancer và proxy phải được cấu hình để không cắt kết nối "im lặng" quá lâu. Vì vậy người ta hay gửi tin heartbeat (ping) định kỳ.
Đây là lý do polling vẫn là lựa chọn đúng trong nhiều trường hợp: nó không cần server giữ gì cả.
So sánh bốn cách
| Tiêu chí | Polling | Long polling | SSE | WebSocket |
|---|---|---|---|---|
| Ai mở kết nối? | Client | Client | Client | Client |
| Ai gửi dữ liệu mới? | Server, chỉ khi được hỏi | Server, khi có tin (trong request đang treo) | Server, bất cứ lúc nào | Cả hai, bất cứ lúc nào |
| Kết nối giữ mở? | Không | Giữ tới khi có tin, rồi mở lại | Có, lâu dài | Có, lâu dài |
| Chiều dữ liệu | Server → client | Server → client | Server → client | Hai chiều |
| Độ trễ | Bằng chu kỳ hỏi | Gần tức thì | Tức thì | Tức thì |
| Giao thức | HTTP | HTTP | HTTP | WebSocket (bắt đầu từ HTTP) |
| Tự kết nối lại | Không cần | Tự viết vòng lặp | Có sẵn | Tự viết |
| Gánh nặng server | Nhiều request | Giữ request treo | Giữ kết nối | Giữ kết nối |
Câu hỏi để phân biệt nhanh:
- Server có trả lời ngay dù không có gì mới không? Có thì là polling.
- Sau mỗi tin, kết nối có kết thúc không? Có thì là long polling. Không thì là SSE hoặc WebSocket.
- Client có gửi dữ liệu lên qua chính kết nối đó không? Có thì là WebSocket, không thì là SSE.
Khi nào dùng cái nào?
Polling: dữ liệu thay đổi chậm và không cần tức thì, như trạng thái đơn hàng, dashboard cập nhật vài phút một lần, kiểm tra kết quả một job đang chạy. Đơn giản là ưu điểm lớn nhất của nó.
Long polling: cần gần realtime nhưng môi trường không hỗ trợ kết nối dài hạn ổn định, hoặc làm phương án dự phòng. Ngày nay hiếm khi là lựa chọn đầu tiên; các thư viện như Socket.IO từng dùng nó làm đường lui khi WebSocket không khả dụng.
SSE: server cần đẩy liên tục, client chủ yếu chỉ nghe: thông báo, giá cổ phiếu, tỷ số trận đấu, tiến độ xử lý, stream câu trả lời AI. Thường đây là lựa chọn bị bỏ quên dù rất hợp.
WebSocket: cả hai bên cùng gửi liên tục: chat, game nhiều người chơi, chỉnh sửa tài liệu cùng lúc, bảng trắng cộng tác.
Những hiểu lầm thường gặp
"Làm realtime thì phải dùng WebSocket." Phần lớn tính năng "realtime" chỉ cần server đẩy xuống, như thông báo hay stream AI. SSE đơn giản hơn, tự kết nối lại, và vẫn là HTTP. Chỉ dùng WebSocket khi client cũng gửi lên dày đặc.
"Long polling chỉ là polling với timeout dài hơn." Khác ở chỗ server chủ động giữ request lại và chỉ trả lời khi có dữ liệu. Với polling, server luôn trả lời ngay. Timeout dài chỉ là chi tiết phụ.
"WebSocket là một phần của HTTP."
WebSocket chỉ mượn HTTP để bắt tay. Sau mã 101 Switching Protocols, kết nối chạy giao thức riêng. Vì vậy những thứ quen thuộc của HTTP như cache, header mỗi request, middleware theo request không còn áp dụng nữa.
"SSE là công nghệ cũ, không ai dùng." SSE đang là cách phổ biến để stream câu trả lời của các API AI. Với HTTP/2, giới hạn số kết nối của nó cũng gần như không còn là vấn đề.
"Giữ kết nối mở thì chẳng tốn gì vì không gửi dữ liệu." Mỗi kết nối mở vẫn chiếm bộ nhớ, file descriptor, và cần cơ chế đồng bộ giữa các instance. Với 100.000 user online, đây là bài toán kiến trúc chứ không còn là chi tiết.
Tóm tắt nhanh
- Server không gọi được trình duyệt, nên mọi kết nối đều do client mở. Bốn cách trong bài là bốn cách để server "nói" qua kết nối đó.
- Polling: hỏi định kỳ, server trả lời ngay. Đơn giản, nhưng trễ và tốn request.
- Long polling: server giữ request tới khi có tin. Nhanh, nhưng mở lại kết nối sau mỗi tin.
- SSE: một kết nối, server đẩy liên tục, một chiều, tự kết nối lại.
- WebSocket: một kết nối, hai chiều, nhanh nhất, nhưng phải tự lo nhiều thứ hơn.
Tự kiểm tra
1. Bạn làm tính năng hiển thị tiến độ export file Excel (0% → 100%) mất khoảng 1 phút. Nên dùng gì?
Đáp án
SSE là lựa chọn hợp nhất: server đẩy tiến độ xuống, client chỉ nghe, và chỉ cần một kết nối. Polling mỗi 2–3 giây cũng chấp nhận được nếu muốn đơn giản tối đa, vì người dùng không cần chính xác từng phần trăm. WebSocket là thừa vì client không cần gửi gì lên.
2. Ứng dụng chat của bạn chạy 3 instance sau load balancer. User A (instance 1) gửi tin cho user B (instance 3) nhưng B không nhận được. Vì sao, và sửa thế nào?
Đáp án
Kết nối WebSocket của B nằm ở instance 3, nên instance 1 nhận tin của A nhưng không có kết nối nào tới B để gửi. Cần một kênh chung giữa các instance, ví dụ Redis Pub/Sub: instance 1 publish tin, cả 3 instance subscribe và instance nào đang giữ kết nối của B sẽ gửi xuống cho B.
3. Vì sao không dùng webhook từ tập trước để báo tin mới cho trình duyệt?
Đáp án
Webhook cần bên nhận có một URL công khai để gọi tới. Trình duyệt không có địa chỉ nào như vậy, và HTTP chỉ cho server trả lời khi có request. Vì thế với trình duyệt, kết nối luôn phải do client mở trước.
Tiếp theo: Đã đi qua 6 thuật ngữ trong 2 tập, bạn có thể xem bản đồ tổng hợp đặt tất cả lên cùng một hình, kèm bảng chọn nhanh cho từng bài toán.