Service vừa sống lại sau 2 giây downtime. 500 client cùng timeout,
cùng retry() ngay lập tức → 500 request đập một lúc.
Dependency gục lại. Bạn vừa biến lỗi tạm thành
thundering herd.
Retry là cần thiết (mạng hỏng, 503, blip). Retry
không có nhịp và không có trần là
backpressure ngược: bạn tự
tăng rate_in đúng lúc hệ thống yếu.
Demo: Retry storm vs backoff + jitter.
Ba tầng của “retry đúng”
- Có nên retry không? — idempotent? idempotency key? Lỗi transient (timeout, 429, 502/503) hay business (400, 401, 404)?
- Chờ bao lâu giữa các lần? — backoff (+ jitter).
- Bao nhiêu lần / bao lâu tổng? — max attempts, tổng budget thời gian (nối timeout budget).
Công thức backoff
// fixed
delay = base
// exponential
delay = base * 2^attempt // 100, 200, 400, 800…
// exponential + full jitter (AWS kiến nghị phổ biến)
delay = random_between(0, min(cap, base * 2^attempt))
// equal jitter (bớt “may rủi” quá thấp)
temp = min(cap, base * 2^attempt)
delay = temp/2 + random_between(0, temp/2)
Jitter phá đồng bộ: 100 client không cùng thức dậy đúng ms thứ 800. Không jitter, exponential vẫn có thể “sóng” — cùng phase sau mỗi lần fail đồng loạt.
Cap (maxDelay) tránh lần sau chờ 30
phút vì exponent. Max attempts tránh retry vô tận
trong loop.
Sketch
async function withRetry(fn, {
baseMs = 100,
maxDelayMs = 5000,
maxAttempts = 5,
shouldRetry = (e) => isTransient(e),
jitter = "full", // full | equal | none
} = {}) {
let lastErr;
for (let attempt = 0; attempt < maxAttempts; attempt++) {
try {
return await fn(attempt);
} catch (e) {
lastErr = e;
if (attempt === maxAttempts - 1 || !shouldRetry(e)) throw e;
const exp = Math.min(maxDelayMs, baseMs * 2 ** attempt);
let delay = exp;
if (jitter === "full") delay = Math.random() * exp;
if (jitter === "equal") delay = exp / 2 + Math.random() * (exp / 2);
await new Promise((r) => setTimeout(r, delay));
}
}
throw lastErr;
}
Khi nào không retry
- 4xx “đúng là sai” — validation, auth, not found (trừ 408/429).
- Side-effect chưa idempotent — charge, gửi mail “một lần” mà không có key → retry = double.
- Caller đã gần hết deadline — retry chỉ để tự timeout tiếp (xem timeout budget).
- Circuit open — retry vào breaker open chỉ đốt client; fail-fast đúng chỗ.
Retry ở đâu trong stack
-
Client / SDK: có budget, honor
Retry-Aftertrên 429. - Queue worker (BullMQ): attempts + backoff của job — đừng vừa worker retry vừa HTTP client retry ×N không kiểm soát.
- Gateway: retry tự động nguy hiểm (nhân traffic); thường để client/queue lo.
Quy tắc thực dụng: một tầng “chịu trách nhiệm retry” cho mỗi hop quan trọng; các tầng khác fail rõ hoặc chỉ retry rất hẹp.
Metric
retry_attemptshistogram / counter theo attempt #retry_give_up- Correlation: spike retry vs latency dependency
Một câu để nhớ
Retry ngay = herd. Backoff + jitter = nhường dependency thở. Cap + max attempts + idempotency = không tự bắn vào chân.
Ghép breaker (đừng gọi khi đỏ), budget thời gian (đừng retry khi hết giờ), và idempotency (đừng nhân side-effect): bộ ba tối thiểu cho mọi client gọi qua mạng.