- Enes Akar (co-founder Upstash) vừa chia sẻ một pattern đơn giản nhưng dễ bị bỏ qua: trong hệ thống retry đa tenant, endpoint treo 30 giây tốn gấp hàng nghìn lần endpoint chết hẳn.
- Câu trả lời của QStash là HostBlocker — cô lập sự cố của một user để không trở thành vấn đề của tất cả.
TL;DR
Retry một endpoint đã chết hẳn là chuyện bình thường. Retry một endpoint treo 30 giây lại là một resource leak rõ rệt — vì mỗi lần thử pin một worker trong pool dùng chung. Enes Akar, co-founder của Upstash, vừa hé lộ cách QStash xử lý: một cơ chế HostBlocker — nếu endpoint của bạn ngừng phản hồi, họ ngừng gọi nó trong một khoảng thời gian. Lỗi của một user không còn kéo theo cả hệ thống. Đây là circuit breaker + bulkhead áp dụng đúng chỗ, và mọi backend team chạy outbound HTTP ở quy mô lớn nên tự hỏi: HostBlocker của mình ở đâu?
Điều gì vừa được chia sẻ
Trong một post ngắn trên X, Enes Akar đóng khung vấn đề gọn đến mức ai làm queue hay webhook delivery đều gật đầu:
Retrying a dead endpoint is fine. Retrying an endpoint that hangs for 30s is a resource leak. We implemented a HostBlocker. If you stop responding, we stop calling.
Không phải một tính năng mới hào nhoáng, không có giá mới, cũng chưa có trang docs riêng. Nhưng đằng sau hai câu đó là một mental model flip mà nhiều team bỏ sót khi thiết kế hệ thống retry.
Vì sao đây là vấn đề lớn hơn bạn nghĩ
Khi tính chi phí retry, kỹ sư hay ngầm giả định mỗi lần thử tốn O(1): một request đi, một response về, xong. Giả định này đúng với endpoint chết hẳn — DNS NXDOMAIN, ECONNREFUSED, hoặc 5xx trong vài mili-giây — vì TCP fail nhanh.
Giả định này sai hoàn toàn với endpoint treo. Endpoint accept TCP, giữ connection, rồi không bao giờ trả lời. Mỗi attempt tốn đúng bằng Max HTTP Response Duration của plan (ví dụ 30 giây) cho tới khi timeout. Trong một worker pool dùng chung, N delivery treo đồng thời = N worker bị pin đứng im 30 giây.
Công thức thật ra là:
- Chi phí retry endpoint chết:
O(1)— vài ms cho TCP handshake fail - Chi phí retry endpoint treo:
O(timeout)— toàn bộ thời gian chờ tối đa
Chênh lệch có thể lên tới hàng nghìn lần. Và trong một hệ đa tenant, webhook của một customer bị treo là đủ để ăn sạch worker pool, kéo chậm delivery cho tất cả khách hàng khác. Đây chính là noisy neighbor antipattern kinh điển mà Azure Architecture Center từng mô tả.
Vì sao exponential backoff thôi là không đủ
Nhiều team nghĩ backoff giải quyết được retry storm, và đúng — với endpoint chết. QStash mặc định dùng công thức delay = min(86400, e^(2.5 * n)), tức là lần retry sau cách ~12s, 2m28s, 30m8s, 6h7m6s. Backoff giúp giãn các attempt ra theo thời gian.
Nhưng với endpoint treo, giãn thời gian không giải phóng worker slot trong lúc một attempt đang chạy. Mỗi khi cửa sổ retry mở, một worker lại đi vào trạng thái treo 30 giây. Backoff trì hoãn lần hỏng tiếp theo; nó không bảo vệ bạn khỏi lần hỏng hiện tại.
Muốn đóng lỗ hổng này bạn cần một pre-dispatch check: trước khi worker nhấc job ra khỏi queue, hỏi: "host này dạo này có ngoan không?". Nếu không, park job lại, đi làm việc khác. Đó đúng là hình dáng của HostBlocker.
So sánh với các pattern liên quan
| Pattern | Áp dụng ở | Cái bảo vệ được |
|---|---|---|
| Circuit breaker (Hystrix, Resilience4j) | Client-side, mỗi dependency | Bảo vệ chính client khỏi cascade failure |
| Bulkhead | Partition static cho mỗi tenant | Cô lập lỗi nhưng lãng phí tài nguyên khi idle |
| Exponential backoff | Giữa các lần retry | Chống retry storm cho endpoint chết |
| HostBlocker | Server-side, pre-dispatch, per-host | Cô lập endpoint treo trong worker pool dùng chung |
HostBlocker gần nhất với circuit breaker, nhưng đặt ở phía platform thay vì phía ứng dụng — key theo host của customer, không phải theo client library của developer. Cách làm tương tự cũng được Inngest mô tả khi họ giải bài toán concurrency đa tenant của mình.
Khi nào bạn cần HostBlocker của riêng mình
- Hệ thống job queue và webhook delivery (QStash, Trigger.dev, Hookdeck, Sidekiq, Celery cho webhook): use case gốc.
- API gateway đứng trước nhiều upstream — một downstream chậm đủ sức ăn hết connection pool.
- Hệ thống fan-out notification (email, SMS, push) gọi callback URL do customer cung cấp.
- Uptime / monitoring checker — target treo nên bị park thay vì retry nóng.
- Bất kỳ nền tảng nào chạy N worker phục vụ M customer với M > N (tức là mọi hệ ở quy mô).
Giới hạn và đánh đổi
- Đây là tweet chia sẻ pattern, không phải announcement chính thức. Chưa có API public để tune ngưỡng chặn, cửa sổ cool-down, hay logic recovery.
- Chặn quá aggressive thì endpoint vừa phục hồi vẫn phải chờ hết cửa sổ — tăng latency của job hợp lệ.
- Chưa rõ QStash key theo host, URL path, hay customer ID. Mỗi lựa chọn có trade-off khác nhau về false positive.
- Nếu muốn opt-out khỏi retry luôn (ví dụ lỗi logic, không phải lỗi transient), QStash có sẵn cơ chế: response
489kèm headerUpstash-NonRetryable-Error: trueđể đẩy thẳng vào Dead Letter Queue.
Điều đáng mang đi
Nếu team bạn vận hành bất kỳ hệ nào gọi outbound HTTP cho nhiều tenant, hãy audit retry path trong tuần này và hỏi ba câu:
- Một endpoint treo 30 giây của một customer có thể pin bao nhiêu worker slot trong pool chung?
- Code của chúng ta có phân biệt được endpoint chết và endpoint treo không — hay đang đối xử cả hai như nhau với exponential backoff?
- Có pre-dispatch check nào nói "skip host này trong X giây" — hay worker cứ nhấc job và treo?
Câu trả lời thường khiến các team không vui. Nhưng đó chính là phần giá trị nhất của bài note ngắn từ Enes: nó đặt câu hỏi đúng.
Nguồn: Enes Akar trên X, QStash Retry Docs, Microsoft Learn — Noisy Neighbor Antipattern, Inngest — Multi-tenant queueing.