WebSocket vs Socket.IO trong production: cơ chế, kiến trúc và bài toán thực tế

Hoang Vu,10 min read

Ngày 01 tháng 4, 2026

Bài này đi theo hướng WebSocket-first:

  1. WebSocket là protocol lõi của realtime.
  2. Socket.IO là library + protocol riêng để triển khai nhanh hơn.
  3. Các bài toán production quan trọng: reconnect, nghẽn, backpressure, duplicate/order.
  4. So sánh Polling, SSE (Server-Sent Events), WebSocket để chọn đúng cơ chế.

Các thuật ngữ sẽ được giải thích ngay inline bằng tooltip, ví dụ backpressure, jitter, idempotency.

Chú giải viết tắt (abbreviation glossary)

  1. HTTP (HyperText Transfer Protocol)
  2. API (Application Programming Interface)
  3. RFC (Request for Comments)
  4. TCP (Transmission Control Protocol)
  5. SSE (Server-Sent Events)
  6. WS (WebSocket)
  7. DX (Developer Experience)
  8. TTL (Time To Live)
  9. EIO (Engine.IO Protocol Version)
  10. JSON (JavaScript Object Notation)
  11. CDN (Content Delivery Network)
  12. LB (Load Balancer)
  13. IO (Input/Output)
  14. OOM (Out Of Memory, có thể kéo theo OOM Killer)

1. WebSocket là gì?

WebSocket là protocol chuẩn theo RFC (Request for Comments) 6455 cho kết nối hai chiều (full-duplex) giữa client và server.

Flow cơ bản:

  1. Client gửi HTTP (HyperText Transfer Protocol) Upgrade request.
  2. Server trả 101 Switching Protocols.
  3. Connection được giữ lâu (long-lived).
  4. Hai bên gửi/nhận frame realtime mà không cần request/response HTTP mới.

Ví dụ handshake:

GET /ws HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: ...
Sec-WebSocket-Version: 13

Mental model ngắn: Client <-> Server (1 connection sống lâu).

Điều hay bị hiểu nhầm: WebSocket bắt đầu bằng HTTP nhưng sau khi upgrade thì không còn là semantics HTTP API (Application Programming Interface) nữa; lúc này bạn đang vận hành một kênh stream hai chiều stateful.

Sơ đồ lifecycle WebSocket handshake và kết nối hai chiều

WEBSOCKET LIFECYCLE (UPGRADE -> DUPLEX -> CLOSE)

1. GET + Upgrade
2. 101 Switching Protocols
3. Bidirectional frames
4. Close + reconnect policy
ClientServer

Handshake diễn ra trên HTTP, sau đó chuyển sang WebSocket frames.

Khi disconnect, client áp dụng reconnect strategy thay vì reconnect liên tục.

WEBSOCKET CHANNEL vs HTTP REQUEST/RESPONSE

WebSocket

single persistent socket

Low-latency bidirectional frames on one long-lived connection.

HTTP

many short request/response cycles

Each interaction re-pays request overhead and connection coordination.

1.1 Cơ chế handshake WebSocket: browser tự upgrade kiểu gì, và vì sao phải làm như vậy?

Đây là điểm nhiều người hay mơ hồ nhất, nên đi theo kiểu hỏi-đáp ngắn:

Hỏi: Browser "biết" upgrade bằng cách nào?

Khi bạn gọi new WebSocket("wss://..."), browser không gửi JSON API như bình thường. Thay vào đó, network stack của browser đi theo state machine có sẵn trong spec WebSocket:

  1. Tạo một HTTP request mở đầu (handshake request).
  2. Gắn các header bắt buộc để "xin chuyển protocol".
  3. Chờ server xác nhận bằng 101 Switching Protocols.
  4. Nếu hợp lệ thì đổi parser từ HTTP sang WebSocket frame parser.
  5. Nếu không hợp lệ thì fail (onerror/onclose) và không mở socket.

Request thực tế từ browser:

GET /ws HTTP/1.1
Host: realtime.example.com
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Origin: https://app.example.com

Response từ server nếu chấp nhận upgrade:

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

WEBSOCKET HANDSHAKE DEEP DIVE

1. Client gửi GET + Upgrade headers
2. Server validate key/origin/policy
3. Server trả 101 Switching Protocols
4. Browser đổi parser: HTTP -> WS frames
BrowserRealtime Server
GET /ws + Upgrade, Connection, Sec-WebSocket-Key
101 + Sec-WebSocket-Accept -> chuyển sang frame mode

Browser không tự suy đoán protocol. Nó chỉ chuyển sang WebSocket mode khi server trả đúng handshake response theo RFC 6455.

Hỏi: Vì sao phải có các header này, không bỏ được à?

  1. Upgrade: websocket: nói rõ mục tiêu là chuyển protocol, không phải gọi HTTP API thường.
  2. Connection: Upgrade: báo đây là thông tin hop-by-hop (liên quan trực tiếp connection hiện tại).
  3. Sec-WebSocket-Key: nonce từ client để ngăn server/proxy HTTP thường "vô tình" coi request này là hợp lệ.
  4. Sec-WebSocket-Accept: server phải chứng minh hiểu đúng handshake bằng giá trị hash theo RFC.

Hỏi: Nếu thiếu/sai header thì chuyện gì xảy ra?

  1. Server có thể trả 400, 426, hoặc không trả 101.
  2. Browser không chuyển sang WebSocket mode.
  3. Kết quả là socket không open dù endpoint vẫn đang chạy HTTP.

Lưu ý quan trọng: đây là cơ chế upgrade protocol, không phải flow 302 redirect như OAuth.

Sau 101, connection không còn request/response HTTP nữa, mà thành kênh frame WebSocket hai chiều.

1.2 Tại sao đa số dùng HTTP/1.1 để handshake thay vì HTTP/2?

Nói ngắn gọn: vì ổn định hạ tầng end-to-end, không phải vì HTTP/2 "kém".

  1. Cơ chế WebSocket Upgrade kinh điển được định nghĩa rõ trên HTTP/1.1.
  2. HTTP/2 không dùng Connection: Upgrade theo cách của HTTP/1.1.
  3. Dù có hướng đi qua extended CONNECT, nhưng khi chạy qua nhiều lớp proxy/CDN/LB, mức tương thích thực tế thường kém đồng đều hơn.

Tư duy vận hành thực dụng:

  1. Handshake chọn đường chắc ăn nhất (thường HTTP/1.1).
  2. Sau khi 101 thành công thì bạn đã có socket TCP sống lâu, throughput realtime phụ thuộc vào WebSocket frame flow hơn là tranh luận HTTP/1.1 hay HTTP/2 ở pha mở đầu.

1.3 Ping/Pong và keep-alive khác nhau thế nào?

Đây là chỗ dễ gây bug production nhất.

Hỏi: Keep-alive rồi thì cần gì ping/pong nữa?

Vì keep-alive chỉ nói rằng tầng transport chưa đóng connection ngay. Nó không đảm bảo luồng ứng dụng còn khỏe.

Hay nhầm nhất là trộn 2 khái niệm:

  1. keep-alive ở HTTP/TCP: giảm việc đóng/mở socket ở tầng dưới.
  2. ping/pong ở WebSocket: heartbeat ở tầng protocol để phát hiện kết nối zombie/half-open.

Ví dụ thực tế:

  1. Client đổi mạng (Wi-Fi -> 4G), NAT table đổi trạng thái.
  2. Ở app bạn tưởng "vẫn còn socket", nhưng frame thật không đi qua nữa.
  3. Nếu không có ping/pong timeout, UI sẽ treo ở trạng thái giả-sống rất lâu.

Một policy thực tế:

  1. Server ping mỗi 20-30s.
  2. Client phải pong trong timeout (ví dụ 10s).
  3. Quá timeout thì đóng socket chủ động và bật reconnect policy.

HEARTBEAT: PING/PONG + TIMEOUT DETECTION

ClientServer
pong timeout -> reconnect

Ping/Pong đo liveness ở tầng WebSocket protocol, không phải chỉ dựa vào TCP keep-alive.

Khi pong quá hạn, nên close chủ động và kích hoạt reconnect policy để tự phục hồi nhanh.

Điểm cốt lõi: ping/pong giúp self-heal nhanh, giảm thời gian người dùng ở trạng thái "đứt ngầm".


2. Socket.IO là gì?

Socket.IO là library + protocol riêng (không phải WebSocket thuần).

Nó cung cấp abstraction rất thực dụng:

  1. Event API (Application Programming Interface) (emit/on).
  2. auto reconnect.
  3. Rooms/namespaces.
  4. Middleware auth.
  5. Fallback transport.

Điểm quan trọng:

  1. Socket.IO ưu tiên WebSocket.
  2. Nếu không upgrade được thì fallback sang HTTP long polling.
Minh họa Socket.IO fallback từ websocket sang xhr-polling

SOCKET.IO TRANSPORT NEGOTIATION (WS FIRST, POLLING FALLBACK)

Client
Gateway / LB
Socket Server
Attempt #1: websocket upgrade
upgrade OK
If upgrade/auth/proxy fails -> fallback to polling
xhr-polling

Socket.IO sẽ giữ app chạy ổn định bằng cách degrade transport thay vì hard-fail kết nối.

2.1 Hiểu đúng layer của Socket.IO

Socket.IO không chỉ là API emit/on, mà là nhiều tầng protocol:

[ Socket.IO protocol ]

[ Engine.IO ]

[ WebSocket hoặc HTTP (polling) ]

[ TCP (Transmission Control Protocol) ]

Ý nghĩa từng tầng:

  1. Socket.IO protocol: event packet, namespace, ack semantics.
  2. Engine.IO: quản lý transport, heartbeat, upgrade, reconnect behavior.
  3. Transport: WebSocket hoặc HTTP long polling.
  4. TCP: kênh truyền tải byte thực tế.

SOCKET.IO STACK: PROTOCOL -> ENGINE -> TRANSPORT -> TCP

Socket.IO protocol
Engine.IO
WebSocket or HTTP polling
TCP transport
42["chat",{"msg":"hello"}]

Packet ứng dụng đi qua nhiều tầng trước khi thành byte trên wire. Vì vậy debug realtime nên tách lỗi theo từng layer.

Raw WebSocket server chỉ hiểu payload tự định nghĩa; Socket.IO packet cần parser/protocol tương thích ở phía server.

2.2 Trình tự kết nối thực tế của Socket.IO

Socket.IO thường không "nhảy" vào WebSocket ngay lập tức ở mọi môi trường; nó có thể bắt đầu bằng polling để đảm bảo kết nối cơ bản trước.

Vì sao bắt đầu bằng polling ở một số môi trường?

  1. Bước HTTP đầu tiên dễ đi qua các lớp mạng "khó chịu" (proxy doanh nghiệp, firewall chặt).

  2. Dễ xác nhận cookie/CORS/session trước khi nâng cấp.

  3. Nếu hạ tầng chặn WebSocket, app vẫn có mode dự phòng thay vì chết hẳn.

  4. Bắt đầu bằng polling (HTTP thật):

GET /socket.io/?EIO=4&transport=polling

EIO=4 là Engine.IO Protocol Version 4.

  1. Sau đó thử upgrade lên WebSocket:
GET /socket.io/?EIO=4&transport=websocket
Upgrade: websocket
Connection: Upgrade
  1. Nếu upgrade fail do proxy/firewall/network policy:
  2. Giữ kết nối ở polling mode.
  3. Ứng dụng vẫn chạy (degraded), nhưng overhead/latency kém hơn.

HTTP LONG POLLING CYCLE (GET HOLD -> RESPONSE -> NEXT GET)

ClientServer
GET /poll (open)
event response
POST /emit
next GET /poll

Long polling không giữ một duplex channel cố định như WebSocket; nó tạo chuỗi request/response lặp để mô phỏng realtime.

2.3 Wire format: vì sao Socket.IO client không nói chuyện trực tiếp với raw WS server?

Khi bạn gọi:

socket.emit("chat", { msg: "hello" });

Payload trên wire sẽ là packet theo protocol của Socket.IO, ví dụ:

42["chat",{"msg":"hello"}]

Trong đó:

  1. 4: message packet type.
  2. 2: event packet type.
  3. phần còn lại: payload JSON (JavaScript Object Notation) event.

Vì sao lại mã kiểu 42[...] thay vì gửi JSON thuần?

Vì Socket.IO cần multiplex nhiều loại control frame và event frame trong cùng stream:

  1. phân loại packet nhanh (open, ping, pong, event, ack...)
  2. gắn namespace/ack semantics
  3. giữ được behavior thống nhất giữa websocket và polling transport

Đây không phải raw payload tuỳ ý của WebSocket app protocol tự định nghĩa, nên raw WebSocket (WS) server sẽ không tự hiểu nếu không implement đúng parser/protocol tương thích.

SOCKET.IO PACKET FLOW (EMIT -> ENCODE -> TRANSPORT -> HANDLE)

1.socket.emit("chat", payload)
2.Socket.IO encode: 42["chat",{...}]
3.Engine.IO frame over WS/polling
4.Server decode -> route event handler
Wire sample: 42["chat",{"msg":"hello"}]

3. Khác biệt cốt lõi: Socket.IO != WebSocket

Socket.IO không chỉ là wrapper mỏng của WebSocket.

Khác biệt kiến trúc:

  1. WebSocket: protocol chuẩn, low-level.
  2. Socket.IO: framework realtime + protocol riêng trên Engine.IO.
  3. Server WebSocket thuần không nói chuyện trực tiếp với client Socket.IO nếu không có lớp tương thích protocol.

Nói ngắn gọn: Socket.IO thêm lớp ứng dụng bên trên WebSocket để ưu tiên DX (Developer Experience) và reliability, đổi lại có thêm overhead protocol.


4. So sánh nhanh

Tiêu chíWebSocketSocket.IO
LoạiProtocolLibrary + protocol
ChuẩnRFCCustom
TransportChỉ WebSocketWebSocket (WS) + polling fallback
ReconnectTự làmBuilt-in
Event systemKhông có sẵnemit/on
Rooms / namespacesKhông có sẵn
OverheadThấpCao hơn

5. Ví dụ dễ hiểu

5.1 WebSocket thuần (raw)

const ws = new WebSocket("wss://example.com");
 
ws.onmessage = (event) => {
  console.log(event.data);
};
 
ws.onopen = () => {
  ws.send("hello");
};

Đặc điểm: low-level, bạn tự xử lý reconnect, retry, heartbeat, backpressure.

5.2 Socket.IO

import { io } from "socket.io-client";
 
const socket = io("https://example.com", {
  transports: ["websocket", "polling"],
});
 
socket.on("message", (data) => {
  console.log(data);
});
 
socket.emit("message", { hello: "world" });

Đặc điểm: có sẵn event abstraction, reconnect, room/namespace.

5.3 Rooms / namespaces giúp scale broadcast thực tế

Về production, room/namespace không chỉ là "cho tiện code":

  1. Giảm fan-out bằng cách chỉ phát tới nhóm liên quan.
  2. Áp quyền theo domain nghiệp vụ (project:42, org:abc).
  3. Cô lập traffic giữa tenant/module để giảm nhiễu xuyên hệ.

SOCKET.IO ROOMS / NAMESPACES BROADCAST MODEL

Client A
Client B
Client C

Socket Server

Namespace /chat
Namespace /ops
Room project:42 (A, B)

Event phát vào room chỉ broadcast tới socket thuộc room đó, giúp giảm fan-out và kiểm soát quyền theo ngữ cảnh nghiệp vụ.


6. Auto-reconnect trong production: làm đúng để không thành reconnect storm

Reconnect đúng không chỉ là "mất kết nối thì nối lại".

Bạn cần policy rõ ràng:

  1. Exponential backoff + jitter.
  2. Retry cap + degraded mode.
  3. Tách lỗi auth (401/403) và lỗi mạng tạm thời.
  4. Resume stream bằng lastEventId hoặc sequence offset.
  5. Thêm guard chống thundering herd bằng random jitter và retry trần theo thiết bị/mạng.

Pseudo:

let attempts = 0;
 
function nextDelayMs() {
  const base = Math.min(30000, 500 * 2 ** attempts);
  const jitter = Math.floor(Math.random() * 400);
  return base + jitter;
}
 
async function reconnect() {
  attempts += 1;
  await sleep(nextDelayMs());
  connect({ lastEventId });
}

Sau reconnect thành công:

  1. Re-auth và re-subscribe room/channel.
  2. bounded replay (không replay mù quáng).
  3. Đồng bộ lại quyền truy cập hiện tại.
  4. Kiểm tra gap theo sequence để tránh miss event ngầm.

RECONNECT + BACKPRESSURE + BATCH CONTROL

Retry schedule (backoff + jitter)

Không reconnect dồn dập để tránh reconnect storm.

Queue pressure monitor
high-watermark

Khi queue vượt ngưỡng, cần throttle hoặc degrade non-critical events.

Batch + throttle mitigation
emit batch(50ms)

Giảm số lần emit giúp ổn định CPU/network và hạ p99 latency.

AUTH-AWARE RECONNECT FLOW

socket drop
reconnect attempt
auth check
refresh token
re-subscribe
network error
401 -> refresh
rejoin rooms + resume stream

Nếu reconnect nhận 401/403, không retry mù quáng; cần refresh token hoặc yêu cầu login lại.

Sau khi auth hợp lệ, re-subscribe room/channel và resume từ last acknowledged offset.


7. Bài toán thực tế thường gặp với WebSocket/Socket.IO

7.1 Nghẽn socket khi burst traffic

Triệu chứng:

  1. Queue depth tăng nhanh.
  2. Latency tăng đột biến.
  3. Memory tăng vì buffer pending.

Giải pháp:

  1. Batch theo cửa sổ 20-100ms.
  2. Throttle event không critical (typing/presence).
  3. Coalesce state (chỉ gửi state mới nhất).
const buffer = [];
setInterval(() => {
  if (buffer.length === 0) return;
  io.to(roomId).emit("updates:batch", buffer.splice(0, buffer.length));
}, 50);

7.2 Duplicate và ordering

Do retry/reconnect, duplicate là chuyện bình thường.

Phòng tránh:

  1. Gắn eventId.
  2. Dùng dedup cache ở consumer.
  3. Event cần strict order thì dùng sequence number theo stream.
  4. Handler nên mang tính idempotency: gọi lại cùng event vẫn an toàn.
function onEvent(event: { eventId: string; seq: number; payload: unknown }) {
  if (dedupCache.has(event.eventId)) return;
  if (event.seq > lastSeq + 1) requestReplay(lastSeq + 1, event.seq - 1);
 
  applyEvent(event.payload);
  dedupCache.add(event.eventId);
  lastSeq = Math.max(lastSeq, event.seq);
}

ORDERING + DEDUP + REPLAY WINDOW

Incoming events (with duplicate/out-of-order)
#101
#102
#102
#104
#103

Stream có thể bị duplicate hoặc lệch thứ tự sau retry/reconnect.

Consumer policy
1) Reject duplicate eventId
2) Buffer small reordering window
3) Replay from lastAckSeq if gap detected
applied #101
applied #102
applied #103
applied #104

7.3 Backpressure

Khi producer nhanh hơn consumer, cần policy rõ:

  1. Drop oldest (telemetry-like).
  2. Drop newest (ổn định queue).
  3. Tạm pause producer trên ngưỡng.
  4. Tách channel critical/non-critical.

Đây là điểm sống còn trong production: không có policy backpressure thì hệ thống thường chết ở lúc traffic tăng đột biến, không phải lúc traffic bình thường.

7.4 Auth/session mismatch

Case thường gặp:

  1. Token hết hạn lúc socket còn mở.
  2. User logout ở tab khác nhưng socket cũ còn sống.
  3. Quyền đổi nhưng room cũ chưa revoke.

Khuyến nghị:

  1. Re-validate auth khi reconnect.
  2. Validate auth trên action critical.
  3. Server chủ động disconnect socket mất quyền.

8. Polling vs SSE (Server-Sent Events) vs WebSocket

Polling

  1. Client gọi API theo chu kỳ.
  2. Dễ triển khai.
  3. Overhead cao khi cần realtime sát.

SSE

  1. Server -> client một chiều trên HTTP stream.
  2. Tốt cho push feed/log/progress.
  3. Không tự nhiên cho duplex.

WebSocket

  1. Hai chiều, latency thấp.
  2. Linh hoạt nhất cho interactive realtime.
  3. Đòi hỏi kỷ luật vận hành cao hơn.

Decision guide:

  1. Push một chiều: SSE.
  2. Realtime hai chiều + full control: WebSocket thuần.
  3. Realtime hai chiều + ra production nhanh trên Node: Socket.IO.

POLLING vs SSE vs WEBSOCKET (FLOW SHAPES)

Polling

Nhiều request rời rạc theo chu kỳ.

SSE

Push một chiều từ server về client.

WebSocket

Kênh hai chiều liên tục, latency thấp.


9. Hỏi nhanh kiểu "tại sao lại làm như vậy?"

  1. Tại sao không dùng HTTP API polling cho mọi thứ? : Với flow tương tác dày (chat/collab/game), polling tạo overhead request lớn và latency "răng cưa".

  2. Tại sao reconnect phải có jitter, không retry ngay luôn? : Retry cùng lúc từ hàng nghìn client sẽ tạo reconnect storm, tự DDOS gateway/auth service.

  3. Tại sao phải có eventId + dedup, trong khi đã có TCP đảm bảo thứ tự? : TCP chỉ đảm bảo trong một connection. Sau reconnect/retry/app-level resend, duplicate vẫn xảy ra ở tầng ứng dụng.

  4. Tại sao cần sequence + replay window? : Để phát hiện mất gói logic sau reconnect và chỉ xin lại phần thiếu, không phải đồng bộ lại toàn bộ state.

  5. Tại sao auth phải kiểm tra lại khi reconnect? : Vì token/quyền có thể đã thay đổi trong lúc socket cũ rớt mạng hoặc bị treo.

  6. Tại sao Socket.IO fallback polling vẫn đáng giá? : Đó là safety net thực dụng: hệ thống degraded nhưng không "mất realtime hoàn toàn" khi WS bị chặn.


10. Khi nào chọn WebSocket, khi nào chọn Socket.IO?

Chọn WebSocket khi:

  1. Cần performance cao và overhead thấp.
  2. Muốn control protocol hoàn toàn.
  3. Backend custom đa ngôn ngữ (Go, Rust, Java, C++...).

Chọn Socket.IO khi:

  1. Cần dev nhanh trên Node.js ecosystem.
  2. Cần built-in reconnect, room, middleware event.
  3. Chấp nhận thêm abstraction/protocol overhead để đổi lấy tốc độ triển khai.

Rule thực dụng:

  1. Team nhỏ + cần ra nhanh: Socket.IO.
  2. Khối lượng cực lớn + cần tối ưu sâu protocol: WebSocket thuần.

Kết luận

WebSocket là nền tảng protocol của realtime web. Socket.IO là lớp framework/protocol bổ sung để triển khai nhanh và tiện hơn.

Hiểu đúng mối quan hệ này sẽ giúp bạn:

  1. Chọn đúng công nghệ theo bài toán.
  2. Thiết kế đúng policy production (reconnect, dedup, backpressure).
  3. Tránh lỗi kiến trúc khi scale realtime system.

2026 © @hoag/blog.