Chương 21. Webhook, Polling, SSE, WebSocket
Ở các chương trước, ta đã nói về:
- HTTP/API: hỏi trực tiếp và chờ câu trả lời.
- Queue: giao việc rồi xử lý sau.
- Pub/Sub: một sự kiện cho nhiều bên nghe.
- Event Streaming: lưu lịch sử sự kiện để đọc lại.
Chương này nói về một câu hỏi rất đời thường:
> Khi một việc chưa có kết quả ngay, làm sao bên còn lại biết lúc nào nó xong?
Ví dụ AI Judge:
Học viên nộp bài.
Backend tạo job chấm bài.
Worker gọi Gemini.
Sau 1 phút 30 giây, kết quả có điểm và nhận xét.
Câu hỏi:
> Frontend biết kết quả bằng cách nào?
Có vài cách phổ biến:
- Polling: frontend hỏi lại định kỳ.
- Webhook: hệ thống khác làm xong thì gọi báo lại cho ta.
- SSE: server đẩy cập nhật một chiều xuống client.
- WebSocket: client và server giữ kết nối hai chiều realtime.
Những thứ này rất hay bị nói lẫn với nhau.
Thực ra chúng trả lời những nhu cầu khác nhau.
Chương này sẽ giữ mọi thứ đơn giản:
> Ai hỏi ai, ai gọi ai, kết nối có giữ lâu không, một chiều hay hai chiều, và khi nào nên dùng.
---
21.1. Một ví dụ xuyên suốt: chấm bài AI
Ta bắt đầu bằng luồng quen thuộc:
Frontend
-> POST /submissions
-> Backend tạo Submission + GradingJob
-> Queue
-> Worker gọi Gemini
-> Lưu kết quả
Sau đó frontend cần biết:
Job đang chờ?
Job đang chạy?
Job đã xong?
Job lỗi?
Điểm là bao nhiêu?
Feedback là gì?
Có bốn cách chính.
Cách 1: Polling
Frontend hỏi định kỳ:
GET /grading-jobs/job_123
GET /grading-jobs/job_123
GET /grading-jobs/job_123
Khi job xong, response có kết quả.
Cách 2: SSE
Frontend mở một kết nối nghe cập nhật.
Server đẩy xuống:
grading_job_running
grading_job_completed
Cách 3: WebSocket
Frontend và server giữ kết nối hai chiều.
Server gửi trạng thái xuống.
Frontend cũng có thể gửi hành động lên qua cùng kết nối.
Cách 4: Webhook
Webhook thường không phải frontend dùng trực tiếp.
Webhook là khi một hệ thống khác gọi lại backend của ta.
Ví dụ payment gateway:
Payment Gateway -> POST /webhooks/payment
Hoặc nếu AI Judge là service riêng:
AI Judge Service -> POST /webhooks/grading-completed của Main App
Nhìn vậy là đã thấy:
- Polling/SSE/WebSocket thường dùng giữa frontend và backend.
- Webhook thường dùng giữa server với server.
---
21.2. Polling là gì?
Polling nghĩa là hỏi lại định kỳ.
Ví dụ:
Frontend: Job xong chưa?
Backend: Chưa.
3 giây sau
Frontend: Job xong chưa?
Backend: Chưa.
3 giây sau
Frontend: Job xong chưa?
Backend: Xong rồi, đây là kết quả.
Trong API:
GET /grading-jobs/job_123
Response khi chưa xong:
{
"job_id": "job_123",
"status": "RUNNING"
}
Response khi xong:
{
"job_id": "job_123",
"status": "SUCCEEDED",
"score": 8.5,
"feedback": "..."
}
Polling rất dễ hiểu.
Nó giống việc thỉnh thoảng nhìn vào bảng số thứ tự xem đã đến lượt mình chưa.
---
21.3. Khi nào Polling là đủ tốt?
Polling phù hợp khi:
- Kết quả không cần realtime từng mili giây.
- Job thường mất vài giây đến vài phút.
- Số user không quá lớn.
- Muốn triển khai đơn giản.
- Client có thể chấp nhận chờ vài giây để thấy cập nhật.
Ví dụ:
- Chấm bài AI.
- Tạo báo cáo.
- Upload và xử lý file.
- Export dữ liệu.
- Kiểm tra trạng thái payment.
- Kiểm tra trạng thái order.
Với AI Judge, polling mỗi 3-5 giây thường đã đủ tốt.
Học viên không cần biết chính xác giây thứ 90.000 job xong.
Chỉ cần UI nói:
Đang chấm...
rồi vài giây sau cập nhật kết quả.
Đừng vội dùng WebSocket nếu polling đã đủ.
---
21.4. Polling có nhược điểm gì?
Polling tạo request lặp lại.
Nếu có 10.000 người cùng mở màn hình và mỗi người polling mỗi 2 giây:
10.000 / 2 = 5.000 request/giây
Rất lớn.
Nhiều request có thể trả cùng một câu:
Chưa xong.
Nhược điểm:
- Tốn request.
- Tốn database/cache nếu không tối ưu.
- Cập nhật không tức thì.
- Nếu interval quá ngắn, tải cao.
- Nếu interval quá dài, người dùng thấy chậm.
Polling đơn giản, nhưng không miễn phí.
---
21.5. Polling nên thiết kế thế nào cho đỡ tốn?
Một vài cách thực dụng:
Cách 1: Poll ít hơn
Ví dụ:
3-5 giây/lần
thay vì:
0.5 giây/lần
Cách 2: Dừng polling khi xong
Khi status là:
SUCCEEDED
FAILED
CANCELLED
thì frontend dừng hỏi.
Cách 3: Backoff
Ban đầu hỏi nhanh.
Sau đó hỏi chậm dần.
Ví dụ:
2s -> 3s -> 5s -> 10s
Cách 4: Endpoint nhẹ
Endpoint status không nên load quá nhiều dữ liệu.
GET /grading-jobs/{id}/status
có thể chỉ trả:
status
updated_at
progress
Khi xong mới gọi endpoint lấy kết quả đầy đủ.
Cách 5: Cache ngắn nếu phù hợp
Với status update không quá nhạy, có thể cache rất ngắn để giảm tải.
Nhưng phải cẩn thận với dữ liệu riêng tư.
---
21.6. Long polling là gì?
Long polling là polling nhưng server giữ request lâu hơn một chút.
Thay vì:
Client hỏi
Server trả ngay: chưa có gì
Long polling:
Client hỏi
Server giữ request tối đa 30 giây
Nếu có thay đổi thì trả ngay
Nếu hết 30 giây vẫn không có gì thì trả "chưa có gì"
Client lại hỏi tiếp
Long polling giảm số request rỗng.
Nó từng rất phổ biến trước khi SSE/WebSocket dễ dùng hơn.
Nhưng nó vẫn có chi phí:
- Giữ connection lâu.
- Server phải quản lý nhiều request đang mở.
- Cần timeout rõ.
Ngày nay, nếu cần server push một chiều, SSE thường dễ hiểu hơn long polling.
---
21.7. Webhook là gì?
Webhook là khi một hệ thống gọi HTTP đến hệ thống khác để báo rằng một sự kiện đã xảy ra.
Nói dễ hiểu:
> Thay vì ta hỏi mãi "xong chưa?", bên kia làm xong thì gọi báo cho ta.
Ví dụ payment:
Payment Gateway -> POST /webhooks/payment
Payload:
{
"event_type": "payment_succeeded",
"payment_id": "pay_123",
"order_id": "order_456",
"amount": 350000
}
Ví dụ AI Judge service riêng:
AI Judge Service -> POST /webhooks/grading
Payload:
{
"event_type": "grading_completed",
"grading_job_id": "job_123",
"score": 8.5
}
Webhook thường dùng server-to-server.
Nó không phải cách frontend nhận realtime trực tiếp.
---
21.8. Webhook khác Polling ở đâu?
Polling:
Ta hỏi bên kia định kỳ.
Webhook:
Bên kia gọi báo cho ta khi có chuyện xảy ra.
So sánh:
| Câu hỏi | Polling | Webhook | |---|---|---| | Ai chủ động? | Bên muốn biết | Bên có sự kiện | | Dùng cho frontend? | Có | Thường không trực tiếp | | Dùng server-to-server? | Có thể | Rất phổ biến | | Tốn request khi chưa có gì? | Có | Ít hơn | | Cần public endpoint? | Không nhất thiết | Có | | Cần verify security? | Có, nhưng đơn giản hơn | Rất cần |
Ví dụ payment gateway không thể để từng backend hỏi liên tục:
Thanh toán xong chưa?
Thanh toán xong chưa?
Thanh toán xong chưa?
Webhook hợp hơn:
Thanh toán xong rồi, tôi gọi báo bạn.
---
21.9. Webhook cần bảo mật như thế nào?
Webhook là endpoint public hoặc ít nhất endpoint nhận từ bên ngoài.
Không được tin payload chỉ vì nó gửi đến đúng URL.
Cần:
- Verify signature.
- Kiểm tra timestamp để chống replay.
- Dùng HTTPS.
- Giới hạn IP nếu provider hỗ trợ.
- Validate payload.
- Idempotency.
- Log raw event nếu cần debug.
Ví dụ payment gateway thường gửi header:
X-Signature: ...
X-Timestamp: ...
Backend dùng secret để kiểm tra chữ ký.
Nếu signature sai, bỏ qua.
Đừng xử lý payment webhook chỉ vì body nói:
payment_succeeded
Ai cũng có thể gửi body như vậy nếu không verify.
---
21.10. Webhook phải idempotent
Webhook có thể gửi trùng.
Provider có thể retry nếu:
- Backend timeout.
- Backend trả 500.
- Mạng lỗi.
- Provider không chắc ta đã nhận được.
Vì vậy webhook handler phải xử lý trùng an toàn.
Ví dụ:
payment_succeeded event_id = evt_123
Nếu nhận evt_123 hai lần, không được ghi payment hai lần, không được tạo invoice hai lần, không được cấp quyền hai lần theo cách gây lỗi.
Cách làm:
- Lưu
event_idđã xử lý. - Unique constraint theo
payment_id. - Kiểm tra trạng thái payment hiện tại.
- Dùng idempotency key.
Webhook mà không idempotent là nguồn lỗi rất thật.
---
21.11. Webhook nên trả response nhanh
Webhook endpoint không nên làm quá nhiều việc nặng ngay trong request.
Ví dụ không tốt:
Receive payment webhook
-> verify
-> update payment
-> update order
-> create invoice PDF
-> send email
-> call CRM
-> update analytics
-> return 200
Nếu email hoặc CRM chậm, provider có thể timeout và retry webhook.
Cách tốt hơn:
Receive webhook
-> verify signature
-> lưu raw event / event record
-> enqueue ProcessPaymentWebhook
-> return 200 nhanh
Worker xử lý tiếp:
ProcessPaymentWebhook
-> update payment
-> publish PaymentSucceeded
-> các bên khác phản ứng
Webhook nên giống quầy tiếp nhận:
> Nhận đúng, ghi lại, rồi xử lý có kiểm soát.
---
21.12. Webhook và thứ tự sự kiện
Webhook có thể đến sai thứ tự.
Ví dụ payment provider gửi:
payment_succeeded
refund_issued
Nhưng backend nhận:
refund_issued
payment_succeeded
Hoặc nhận event cũ sau event mới.
Vì vậy handler cần:
- Kiểm tra trạng thái hiện tại.
- Dùng timestamp/version nếu provider có.
- Không cho trạng thái đi lùi sai.
- Có reconciliation job.
- Có thể fetch trạng thái cuối từ provider nếu nghi ngờ.
Với payment, không nên xử lý webhook theo kiểu:
Nhận event nào thì set status y chang event đó.
Phải có state machine và luật chuyển trạng thái.
---
21.13. Webhook và reconciliation
Webhook có thể mất hoặc xử lý lỗi.
Vì vậy với nghiệp vụ quan trọng, không nên chỉ dựa vào webhook.
Cần reconciliation.
Ví dụ:
Mỗi 10 phút:
tìm payment đang PENDING quá lâu
gọi provider hỏi trạng thái thật
cập nhật nếu cần
Hoặc:
So sánh danh sách giao dịch provider với database nội bộ mỗi ngày.
Webhook cho tốc độ.
Reconciliation cho độ chắc.
Đặc biệt với:
- Payment.
- Refund.
- Payout.
- Subscription.
- Delivery status quan trọng.
Webhook là một tín hiệu, không phải luôn là sự thật duy nhất.
---
21.14. SSE là gì?
SSE là Server-Sent Events.
Nói dễ hiểu:
> Browser mở một kết nối đến server, rồi server đẩy sự kiện xuống browser một chiều.
Client mở:
GET /events
Server giữ kết nối và gửi:
event: grading_status
data: {"job_id":"job_123","status":"RUNNING"}
event: grading_completed
data: {"job_id":"job_123","score":8.5}
SSE là một chiều:
Server -> Client
Client muốn gửi dữ liệu lên server thì vẫn dùng HTTP request bình thường.
SSE rất hợp khi server cần cập nhật trạng thái xuống frontend, nhưng frontend không cần gửi liên tục qua cùng kết nối.
---
21.15. Khi nào dùng SSE?
SSE phù hợp khi:
- Cần server push một chiều.
- Cần cập nhật trạng thái job.
- Cần notification nhẹ.
- Cần dashboard cập nhật liên tục.
- Muốn đơn giản hơn WebSocket.
- Chủ yếu chạy trên browser.
Ví dụ:
- AI grading status.
- Build/deploy progress.
- Report generation progress.
- Notification list.
- Live dashboard nhẹ.
- Log stream đơn giản.
Luồng AI Judge:
Frontend mở SSE /events
Worker chấm xong
Backend gửi grading_completed xuống SSE
Frontend cập nhật UI
Nếu kết nối mất, frontend có thể reconnect.
SSE có cơ chế Last-Event-ID để hỗ trợ nối lại nếu server thiết kế.
---
21.16. SSE có ưu điểm gì?
SSE đơn giản hơn WebSocket trong nhiều trường hợp.
Ưu điểm:
- Dùng HTTP thông thường.
- Browser hỗ trợ
EventSource. - Tự reconnect tương đối dễ.
- Phù hợp server push một chiều.
- Dễ debug hơn WebSocket.
- Ít phức tạp hơn khi không cần hai chiều.
Nếu nhu cầu chỉ là:
Server báo job đã xong.
Server báo notification mới.
Server đẩy progress.
SSE thường rất vừa.
Không cần WebSocket cho mọi thứ realtime.
---
21.17. SSE có nhược điểm gì?
SSE cũng có giới hạn:
- Chủ yếu một chiều.
- Không phù hợp binary data.
- Mỗi client giữ connection lâu.
- Cần server/proxy cấu hình không buffer.
- Cần xử lý reconnect.
- Với traffic rất lớn, cần hạ tầng phù hợp.
Một vấn đề thực tế:
Reverse proxy hoặc load balancer có thể buffer response.
Nếu buffer, event không đến ngay.
Cần cấu hình đúng.
Ngoài ra, nếu có nhiều server instance, cần biết user đang kết nối server nào hoặc dùng Pub/Sub backend để fanout.
---
21.18. WebSocket là gì?
WebSocket là kết nối hai chiều lâu dài giữa client và server.
Sau khi mở kết nối:
Client <-> Server
Cả hai bên đều có thể gửi message bất cứ lúc nào.
Ví dụ chat:
User A gửi tin nhắn
Server nhận
Server đẩy tin nhắn đến User B
User B gửi typing
Server đẩy typing đến User A
WebSocket hợp với realtime hai chiều:
- Chat.
- Multiplayer game.
- Collaborative editing.
- Live cursor.
- Trading dashboard tương tác.
- Presence/typing indicator.
- Realtime support.
Nếu client chỉ cần nhận cập nhật một chiều, SSE có thể đơn giản hơn.
---
21.19. Khi nào dùng WebSocket?
WebSocket phù hợp khi:
- Cần hai chiều realtime.
- Client gửi message thường xuyên.
- Server đẩy message thường xuyên.
- Độ trễ thấp quan trọng.
- Kết nối tương tác kéo dài.
Ví dụ:
Chat app
Game realtime
Collaborative document
Live support
Trading interface
Presence system
Với AI Judge, WebSocket thường chỉ cần nếu:
- Có nhiều cập nhật realtime.
- Người dùng tương tác trong lúc job chạy.
- App đã có WebSocket sẵn.
- Muốn gom notification realtime qua một kênh chung.
Nếu chỉ báo "chấm xong", polling hoặc SSE thường đủ.
---
21.20. WebSocket có chi phí gì?
WebSocket không chỉ là đổi endpoint.
Nó kéo theo:
- Kết nối lâu dài.
- Quản lý user đang online.
- Heartbeat/ping-pong.
- Reconnect.
- Authentication khi connect.
- Authorization theo channel/room.
- Scale nhiều server.
- Fanout message.
- Backpressure nếu client chậm.
- Monitoring connection count.
Với HTTP request, request đến rồi đi.
Với WebSocket, connection sống lâu.
Server phải nhớ:
User nào đang nối?
Nối vào server nào?
Được subscribe room nào?
Nếu mất mạng thì sao?
Nếu mở nhiều tab thì sao?
WebSocket mạnh, nhưng vận hành phức tạp hơn polling và SSE.
---
21.21. WebSocket và Pub/Sub backend
Khi chỉ có một server, WebSocket đơn giản hơn.
Nhưng production thường có nhiều server:
ws-server-1
ws-server-2
ws-server-3
User A có thể đang nối vào server 1.
User B đang nối vào server 3.
Nếu một event xảy ra ở server 2, làm sao gửi đến đúng user?
Cần Pub/Sub backend:
Application publish notification:user_123
ws-server nào giữ connection user_123 thì gửi ra socket
Công cụ có thể là:
- Redis Pub/Sub.
- Redis Streams.
- NATS.
- Kafka.
- Managed realtime service.
Với signal realtime tạm thời, Redis Pub/Sub có thể ổn.
Với notification quan trọng, phải lưu DB trước.
---
21.22. WebSocket không thay thế database
Một lỗi phổ biến:
> Gửi qua WebSocket rồi là xong.
Không đúng nếu thông báo quan trọng.
Ví dụ:
GradingCompleted
-> gửi WebSocket cho học viên
Nếu học viên offline thì sao?
Nếu tab bị đóng thì sao?
Nếu mạng mất đúng lúc đó thì sao?
Vì vậy:
GradingCompleted
-> lưu kết quả vào database
-> tạo notification record nếu cần
-> nếu user online, gửi WebSocket/SSE signal
Realtime signal chỉ là cách báo nhanh.
Database mới là nơi user mở lại vẫn thấy kết quả.
---
21.23. Polling, SSE, WebSocket: so sánh nhanh
| Câu hỏi | Polling | SSE | WebSocket | |---|---|---|---| | Client nhận cập nhật thế nào? | Hỏi định kỳ | Server đẩy xuống | Server và client gửi qua lại | | Kết nối giữ lâu không? | Không | Có | Có | | Một chiều hay hai chiều? | Client hỏi | Server -> Client | Hai chiều | | Dễ triển khai | Rất dễ | Trung bình | Khó hơn | | Realtime | Gần realtime | Realtime một chiều | Realtime hai chiều | | Phù hợp | Job status, báo cáo | Progress, notification, dashboard | Chat, game, collaboration | | Chi phí vận hành | Thấp đến vừa | Vừa | Cao hơn |
Không có cách nào luôn tốt nhất.
Câu hỏi là:
> Trải nghiệm cần realtime đến mức nào, và hệ thống có đáng nhận thêm độ phức tạp không?
---
21.24. Webhook, SSE, WebSocket khác nhau ở hướng kết nối
Một cách nhớ rất dễ:
Polling:
Client hỏi server.
Webhook:
Server khác gọi server của ta.
SSE:
Server đẩy xuống browser một chiều.
WebSocket:
Browser và server nói chuyện hai chiều.
Ví dụ AI Judge:
Frontend polling:
Browser hỏi Backend: job xong chưa?
SSE:
Backend báo Browser: job xong rồi.
WebSocket:
Browser và Backend giữ kênh realtime chung.
Webhook:
AI Judge Service gọi Main App: job xong rồi.
Chỉ cần nhớ hướng gọi là đã tránh được rất nhiều nhầm lẫn.
---
21.25. Webhook dùng cho service-to-service
Webhook thường xuất hiện khi bạn tích hợp với hệ thống ngoài.
Ví dụ:
- Payment gateway báo thanh toán thành công.
- GitHub báo push mới.
- Stripe báo subscription renewed.
- Delivery provider báo giao hàng thất bại.
- AI Judge service riêng báo chấm xong.
Webhook là cách bên ngoài nói:
Có sự kiện xảy ra ở phía tôi.
Đây là thông tin.
Backend của ta nhận, verify, lưu, rồi xử lý.
Không nên để frontend nhận webhook trực tiếp.
Frontend không phù hợp để làm endpoint đáng tin cho hệ thống ngoài.
---
21.26. Polling dùng cho client kiểm tra trạng thái
Polling rất hợp khi client cần kiểm tra một trạng thái có sẵn trong backend.
Ví dụ:
GET /grading-jobs/job_123
GET /report-jobs/report_456
GET /imports/import_789
Backend trả trạng thái hiện tại từ database/cache.
Polling không cần server nhớ connection.
Điều này làm nó đơn giản và bền.
Nếu user refresh trang, frontend chỉ cần gọi lại endpoint.
Nếu user offline, không sao.
Khi mở lại, gọi API và thấy trạng thái mới nhất.
Vì vậy với nhiều job nền, polling là lựa chọn đầu tiên rất hợp lý.
---
21.27. SSE dùng cho server push một chiều
SSE nằm giữa polling và WebSocket.
Nó tốt khi:
- Polling hơi phí.
- WebSocket hơi nặng.
- Server chỉ cần đẩy cập nhật xuống.
Ví dụ:
Report progress
Build log
AI grading status
Notification feed
Dashboard metrics
Client vẫn dùng HTTP bình thường để gửi command:
POST /submissions
POST /orders
POST /reports
SSE chỉ dùng để nhận cập nhật:
GET /events
Tách như vậy rất dễ hiểu.
---
21.28. WebSocket dùng cho tương tác hai chiều
WebSocket đáng dùng khi cả hai bên nói chuyện liên tục.
Ví dụ chat:
Client gửi message
Server gửi message mới
Client gửi typing
Server gửi read receipt
Ví dụ collaborative editor:
Client gửi operation
Server broadcast operation
Client nhận cursor của người khác
Ví dụ game:
Client gửi input
Server gửi state update
Nếu app không có nhu cầu như vậy, WebSocket có thể là quá tay.
Đừng dùng WebSocket chỉ vì nghe realtime.
---
21.29. AI Judge nên dùng gì?
Với AI Judge thông thường, thiết kế thực dụng:
POST /submissions
-> tạo grading_job
-> trả job_id
Sau đó:
Cách đơn giản nhất
Frontend polling GET /grading-jobs/{id} mỗi 3-5 giây
Phù hợp cho phần lớn trường hợp.
Cách đẹp hơn một chút
SSE để server báo GradingCompleted
Frontend vẫn có thể fallback về polling.
Khi đã có realtime platform
WebSocket notification
Nhưng kết quả vẫn phải lưu DB.
Nếu AI Judge là service riêng
AI Judge Service webhook về Main App
Hoặc Main App nghe event từ queue/pubsub.
Tóm lại:
Client biết kết quả: Polling/SSE/WebSocket.
Service báo service: Webhook/PubSub/Queue.
Job chấm bài: Queue.
Kết quả bền vững: Database.
---
21.30. Payment nên dùng gì?
Payment là ví dụ kinh điển của webhook.
Luồng thường gặp:
Backend tạo payment
-> trả payment_url cho frontend
-> user thanh toán ở gateway
-> gateway gọi webhook về backend
-> backend xác nhận payment
-> order chuyển PAID
Frontend có thể:
- Poll order/payment status.
- Hoặc được redirect về trang kết quả.
- Hoặc nhận SSE/WebSocket notification nếu app có realtime.
Nhưng trạng thái payment thật nên đến từ:
Webhook đã verify
hoặc backend query provider để đối soát
Không nên tin frontend nói:
Tôi thanh toán xong rồi.
Payment là nơi server-to-server verification rất quan trọng.
---
21.31. Chat nên dùng gì?
Chat cần realtime hai chiều.
WebSocket thường phù hợp.
Luồng:
Client A -> WebSocket -> Server: gửi message
Server -> lưu message DB
Server -> WebSocket -> Client B: message mới
Cần:
- Lưu message vào database.
- Xử lý user offline.
- Push notification nếu offline.
- WebSocket cho user online.
- Pub/Sub backend nếu nhiều server.
Không nên chỉ broadcast message qua WebSocket mà không lưu.
User mở lại app sẽ mất lịch sử chat.
---
21.32. Dashboard realtime nên dùng gì?
Tùy mức realtime.
Nếu dashboard cập nhật mỗi 30 giây:
Polling
là đủ.
Nếu dashboard cần server đẩy số liệu mỗi vài giây:
SSE
rất hợp.
Nếu dashboard có tương tác hai chiều nhiều, filter live, command control:
WebSocket
có thể phù hợp.
Đừng lấy WebSocket làm mặc định.
Hãy hỏi:
> Người dùng cần thấy cập nhật trong bao lâu? 1 giây, 5 giây, 30 giây?
Nhu cầu trải nghiệm quyết định kỹ thuật.
---
21.33. Notification nên dùng gì?
Notification tốt thường có hai phần:
Phần bền
notification_records trong database
User mở app sau vẫn thấy.
Phần realtime
Nếu user online:
SSE/WebSocket signal
để UI cập nhật ngay.
Nếu user offline:
Email / push notification
Luồng:
GradingCompleted
-> Notification service tạo record
-> Nếu user online, gửi realtime signal
-> Nếu cần, enqueue email/push
Realtime chỉ là cách báo nhanh.
Database mới là nơi lưu sự thật người dùng có thông báo.
---
21.34. Trạng thái hiện tại nên lấy từ đâu?
Dù dùng SSE hay WebSocket, client vẫn nên có API để lấy trạng thái hiện tại.
Ví dụ:
GET /grading-jobs/{id}
GET /notifications
GET /chat/rooms/{id}/messages
Vì:
- Client có thể mở trang sau khi event đã xảy ra.
- Kết nối realtime có thể mất.
- Message có thể đến trễ.
- User có thể refresh.
- Mobile app có thể bị ngủ.
Realtime event giúp UI cập nhật nhanh.
API trạng thái giúp UI đồng bộ lại.
Thiết kế tốt thường có cả hai:
State API + realtime signal
Không chỉ một trong hai.
---
21.35. Reconnect và missed events
Với SSE/WebSocket, connection có thể mất.
Lý do:
- Mạng yếu.
- Laptop sleep.
- Mobile đổi mạng.
- Server deploy.
- Load balancer timeout.
Khi reconnect, client cần biết mình có bỏ lỡ gì không.
Cách đơn giản:
Reconnect xong gọi API lấy trạng thái mới nhất.
Ví dụ:
GET /grading-jobs/job_123
GET /notifications?since=last_seen
Với SSE, có thể dùng Last-Event-ID.
Với WebSocket, có thể dùng sequence number.
Nhưng cách dễ nhất và bền nhất vẫn là:
> Có API đọc trạng thái hiện tại từ database.
Realtime chỉ giúp giảm độ trễ, không nên là nguồn sự thật duy nhất.
---
21.36. Backpressure với realtime
Nếu server gửi nhanh hơn client nhận, message sẽ dồn lại.
Ví dụ:
- Dashboard quá nhiều updates.
- Chat room quá đông.
- Client mạng chậm.
- Browser tab bị treo.
Cần chiến lược:
- Drop message không quan trọng.
- Gộp nhiều update thành một.
- Chỉ gửi state mới nhất.
- Giới hạn tốc độ gửi.
- Ngắt client quá chậm.
- Cho client tự fetch lại state.
Ví dụ dashboard:
Không cần gửi 1.000 update/giây.
Có thể gửi snapshot mỗi 1 giây.
Ví dụ typing indicator:
Mất vài event không sao.
Ví dụ payment status:
Không được chỉ dựa vào realtime event, phải có state API.
---
21.37. Load balancer và sticky session
Với WebSocket/SSE, connection sống lâu.
Khi có nhiều server, load balancer cần xử lý đúng.
Có hai hướng:
Sticky session
User đã vào server nào thì tiếp tục vào server đó.
Dễ hơn cho state trong memory.
Nhưng có thể khó scale và khó phục hồi khi server chết.
Stateless + Pub/Sub backend
Server nào cũng có thể phục vụ.
Thông tin connection/fanout được hỗ trợ bằng Redis/NATS/broker hoặc presence store.
Production thường cần nghĩ đến:
- Connection count mỗi server.
- Deploy không cắt connection quá thô.
- Health check.
- Idle timeout của proxy.
- Reconnect strategy.
Đây là chi phí vận hành của realtime connection.
---
21.38. Timeout và heartbeat
Kết nối lâu cần heartbeat.
Heartbeat là tín hiệu nhỏ để biết hai bên còn sống.
WebSocket thường dùng ping/pong.
SSE có thể gửi comment/keep-alive:
: keep-alive
Nếu không có heartbeat:
- Connection chết nhưng server không biết.
- Proxy đóng kết nối im lặng.
- Client tưởng vẫn đang nghe.
Heartbeat giúp phát hiện mất kết nối và reconnect.
Nhưng heartbeat cũng tạo tải, nên interval phải hợp lý.
---
21.39. Authentication với SSE/WebSocket
Kết nối realtime vẫn cần auth.
Các câu hỏi:
- User là ai?
- Token hết hạn thì sao?
- User được subscribe channel nào?
- Nếu user bị revoke quyền thì ngắt thế nào?
Polling dùng HTTP request nên auth giống API bình thường.
SSE/WebSocket cần kiểm tra khi connect và có thể kiểm tra khi subscribe room/channel.
Không nên để client tự nói:
subscribe user_id=123
mà không kiểm tra user thật sự có quyền nhận dữ liệu của user_id=123.
Realtime leak dữ liệu rất nguy hiểm vì nó diễn ra âm thầm.
---
21.40. Webhook observability
Webhook cần log và monitoring riêng.
Cần biết:
- Provider gửi event nào.
- Signature verify thành công không.
- Event id là gì.
- Xử lý lần thứ mấy.
- Response status trả cho provider.
- Event có bị duplicate không.
- Event có vào DLQ không.
- Payment/order nào liên quan.
Khi có sự cố:
Khách đã thanh toán nhưng order chưa paid.
Ta cần xem:
Webhook có đến không?
Signature có pass không?
Handler có chạy không?
Event có duplicate không?
Payment state hiện tại là gì?
Có reconciliation chưa?
Không có log webhook, debug payment rất mệt.
---
21.41. Polling observability
Polling cũng cần theo dõi.
Cần biết:
- Endpoint status bị gọi bao nhiêu lần.
- QPS từ polling là bao nhiêu.
- Cache hit rate.
- DB load.
- Polling interval thực tế.
- Client có quên dừng polling không.
Một lỗi frontend nhỏ có thể tạo tải rất lớn.
Ví dụ:
Mỗi tab polling mỗi 500ms
User mở 5 tab
10.000 user
Tải tăng rất nhanh.
Polling đơn giản, nhưng vẫn cần kỷ luật.
---
21.42. SSE/WebSocket observability
Cần theo dõi:
- Số connection đang mở.
- Connection per server.
- Message sent rate.
- Message dropped rate.
- Reconnect rate.
- Error rate.
- Latency từ event đến client.
- Client chậm.
- Memory per connection.
Nếu reconnect rate tăng đột biến, có thể:
- Load balancer timeout sai.
- Deploy làm rớt connection.
- Server quá tải.
- Network issue.
Realtime system mà không observability thì rất khó vận hành.
---
21.43. Bảng chọn nhanh
| Tình huống | Cách phù hợp | |---|---| | Chấm bài AI 90 giây, user chờ kết quả | Polling trước, SSE nếu muốn mượt hơn | | Payment gateway báo kết quả | Webhook | | Chat realtime | WebSocket | | Typing indicator | WebSocket hoặc realtime pub/sub tạm thời | | Dashboard cập nhật mỗi 30 giây | Polling | | Dashboard cập nhật mỗi 1-2 giây | SSE | | Notification quan trọng | Lưu DB + SSE/WebSocket signal | | Service ngoài báo delivery failed | Webhook | | Export report vài phút | Polling hoặc SSE progress | | Collaborative editor | WebSocket |
---
21.44. Những lỗi phổ biến
Lỗi 1: Dùng WebSocket cho mọi thứ
Nhiều job status chỉ cần polling.
WebSocket làm hệ thống phức tạp hơn không cần thiết.
Lỗi 2: Chỉ gửi realtime, không lưu database
User offline là mất thông báo/kết quả.
Lỗi 3: Webhook không verify signature
Ai cũng có thể giả mạo event.
Lỗi 4: Webhook không idempotent
Provider retry làm xử lý trùng.
Lỗi 5: Polling quá dày
Frontend vô tình tạo tải lớn.
Lỗi 6: SSE/WebSocket không có reconnect strategy
Mất mạng là UI kẹt ở trạng thái cũ.
Lỗi 7: Không có API lấy trạng thái hiện tại
Client bỏ lỡ event thì không biết đồng bộ lại từ đâu.
Lỗi 8: Nhầm Webhook với WebSocket
Webhook là server gọi server.
WebSocket là client và server giữ kết nối realtime hai chiều.
---
21.45. Checklist thiết kế
Khi cần cập nhật trạng thái, hãy hỏi:
- Ai cần biết cập nhật?
- Client hay server khác?
- Có cần kết quả ngay không?
- Có cần realtime thật không?
- Cập nhật một chiều hay hai chiều?
- Nếu client offline thì sao?
- Có cần lưu kết quả vào database không?
- Có API đọc trạng thái hiện tại không?
- Polling interval bao nhiêu là hợp lý?
- Webhook có verify signature không?
- Webhook có idempotent không?
- SSE/WebSocket có reconnect không?
- Có cần Pub/Sub backend cho nhiều server không?
- Có monitoring connection/message/webhook không?
- Nếu message bị lỡ, hệ thống đồng bộ lại thế nào?
Trả lời các câu này trước khi chọn công nghệ.
---
21.46. Tóm tắt bằng AI Judge
Luồng đơn giản, thực dụng:
Frontend
-> POST /submissions
-> nhận grading_job_id
-> polling GET /grading-jobs/{id} mỗi 3-5 giây
-> dừng khi SUCCEEDED/FAILED
Khi muốn mượt hơn:
Frontend mở SSE /events
Backend gửi GradingCompleted
Frontend cập nhật ngay
Nếu mất SSE, fallback polling
Nếu app đã có realtime platform:
WebSocket gửi notification realtime
Nhưng kết quả vẫn lấy/lưu qua database API
Nếu AI Judge tách thành service riêng:
AI Judge Service
-> webhook hoặc event về Main App
-> Main App lưu kết quả
-> báo frontend bằng polling/SSE/WebSocket
Một câu nhớ nhanh:
> Worker chấm bài bằng queue. Backend lưu kết quả vào database. Frontend biết kết quả bằng polling/SSE/WebSocket. Service ngoài báo về bằng webhook.
---
21.47. Kết luận của chương
Webhook, Polling, SSE và WebSocket đều giải quyết bài toán cập nhật trạng thái, nhưng khác nhau về hướng giao tiếp và mức realtime.
Polling:
> Client hỏi định kỳ. Đơn giản, bền, đủ tốt cho nhiều job nền.
Webhook:
> Server khác gọi báo cho server của ta. Rất phổ biến trong payment và tích hợp hệ thống ngoài.
SSE:
> Server đẩy cập nhật một chiều xuống browser. Hợp với progress, notification, dashboard nhẹ.
WebSocket:
> Client và server giữ kết nối hai chiều realtime. Hợp với chat, game, collaboration, presence.
Thông điệp quan trọng nhất:
> Đừng chọn công nghệ realtime trước. Hãy hỏi ai cần biết, cần biết nhanh đến đâu, một chiều hay hai chiều, nếu bỏ lỡ thì có cách đồng bộ lại không.
Với phần lớn hệ thống thực dụng, bắt đầu bằng polling tốt, thêm SSE khi cần mượt hơn, dùng WebSocket khi thật sự cần hai chiều, và dùng webhook cho server-to-server integration.