Vận hành rate limit — header, retry + jitter, giám sát, shadow mode và ADR
Rate limit là một hợp đồng hai bên. Server quyết định chặn; nhưng chặn xong thì client làm gì mới quyết định hệ thống khá lên hay tệ đi: client retry ngay lập
tức nhân tải lên gấp 69 lần, client ngoan ngoãn chờ đúng Retry-After lại cùng quay về một lúc. Bài cuối của series đi qua cả hai phía trong shop của ví dụ:
phía server (header, ra mắt limit mới an toàn, giám sát, xử lý sự cố) và phía client — cả đối tác gọi vào API của shop, lẫn chính app Laravel của shop gọi ra API
đối tác (vận chuyển, thanh toán).
Thuật ngữ dùng trong bài
Retry-After (header báo client chờ bao nhiêu giây hoặc tới thời điểm nào) · exponential backoff (lần chờ sau gấp đôi lần trước) · jitter (cộng một khoảng ngẫu nhiên vào thời gian chờ để các client không quay lại cùng lúc) · retry storm (bão retry: lỗi sinh ra retry, retry sinh thêm tải, tải sinh thêm lỗi) · thundering herd (đàn đông: rất nhiều client cùng thức dậy một lúc) · retry budget (ngân sách retry: retry không vượt X% số request gốc) · shadow mode / dry run / dark launch (tính toán như thật nhưng không chặn, chỉ ghi log) · kill switch (cần gạt tắt tính năng ngay, không cần deploy) · idempotency (gửi lại nhiều lần cũng chỉ có tác dụng một lần).
Kiểm chứng: RFC 6585 §4; RFC 9110 §10.2.3; draft IETFdraft-ietf-httpapi-ratelimit-headers-11(23/05/2026); AWS Builders' Library "Timeouts, retries, and backoff with jitter" (Marc Brooker); Google SRE Book ch. 21 "Handling Overload"; Stripe 2017; GitHub Docs; Discord Developer Docs; nginx docs (limit_req_dry_run,$limit_req_status,limit_req_log_level); READMEenvoyproxy/ratelimit; source Laravel 12.64 (Http\Client\PendingRequest::retry,Cache\RateLimiting\Limit::response). Đọc ngày 02/10/2026. Lab chạy thật: 1 000 client Node 22 với 5 chiến lược retry (out/06-retry.txt); Laravel 12.64 HTTP client gọi API đối tác giả (out/06-laravel-http.txt); nginx 1.31.3 dry run (out/05-nginx.txt).
Trả lời 30 giây
Phía server: trả 429 +Retry-After(không phải 503), thêm header còn lại bao nhiêu và body nói limiter nào chặn; ra mắt mọi limit mới ở shadow mode trước (nginxlimit_req_dry_runghiREJECTED_DRY_RUNvào log), có cần gạt tắt nhanh, có metric theo limiter × route × khách, và cảnh báo cả khi limiter tự hỏng (fail-open). Phía client: retry phải có giới hạn và có jitter — lab 1 000 client: retry ngay lập tức nhân tải ×68,7; chờ đúngRetry-Afterthì giảm còn ×5,2 nhưng tạo đỉnh 441 request/100ms ở mỗi đầu giây; cộng thêm jitter 0–1 giây thì ×3,9, đỉnh 303 và rải đều. Và đừng tin thư viện mặc định:Http::retry(3, 100)của Laravel bỏ quaRetry-After(3 request cách nhau ~100ms rồi ném 429) và gửi lại cả POST bị 422 — phải tự đọc header và chỉ retry 429/5xx/lỗi mạng.
1. Bài toán gốc và bối cảnh ra đời
Ba hiện tượng làm một hệ thống có rate limit vẫn sập:
- Retry nhân tải. AWS: "If each layer retries independently, the load on the database will increase 243x" (5 tầng, mỗi tầng 3 lần). Client bị chặn mà retry ngay thì mỗi lần chặn sinh thêm một request.
- Đồng bộ hoá vô tình. Client cùng bị chặn trong một giây nhận cùng một
Retry-After(RFC 9110 chỉ cho số nguyên giây) và cùng quay lại. AWS: "If all the failed calls back off to the same time, they cause contention or overload again when they are retried." - Limit mới chặn nhầm khách thật — vì con số sai, key sai, hay một client hợp lệ có hành vi lạ — và không ai biết cho tới khi khách gọi điện.
Phần server (header, rollout, giám sát) và phần client (retry, backoff, jitter) phải được thiết kế cùng nhau; một bên tốt không bù được bên kia.
2. Mô hình tư duy cốt lõi
- Retry là ích kỷ. AWS: "retries are selfish. Retries are a way for clients to assert the importance of their request and demand that the service spend more of its resources to handle it." Nên retry phải có trần (số lần, ngân sách), có khoảng chờ tăng dần, và có jitter.
- Mỗi limit mới là một thay đổi hành vi với khách hàng. Ra mắt như ra mắt tính năng: đo trước (shadow), bật dần, có cần gạt, có người nhìn số.
3. Internals và quyết định thiết kế
3.1 Hợp đồng phía server: trả gì khi chặn
HTTP/1.1 429 Too Many Requests
Retry-After: 12 ← RFC 9110: số nguyên giây (hoặc HTTP-date)
X-RateLimit-Limit: 600 ← kiểu đang phổ biến (GitHub, Discord, Laravel)
X-RateLimit-Remaining: 0
RateLimit-Policy: "partner-pro";q=600;w=60 ← draft IETF 11: q = hạn mức, w = cửa sổ (giây)
RateLimit: "partner-pro";r=0;t=12 ← r = còn lại, t = số giây
Content-Type: application/json
Cache-Control: no-store ← RFC 6585: 429 "MUST NOT be stored by a cache"
{"error":"rate_limited","limiter":"partner-pro","retry_after":12,"docs":"https://…/rate-limits"}
- Dạng header IETF lấy từ draft 11 (ví dụ trong draft:
RateLimit-Policy: "burst";q=100;w=60,"daily";q=1000;w=86400); draft còn đổi, nên nhiều API gửi cảX-RateLimit-*lẫn header mới. Draft còn dặn: khi có cả hai, "the Retry-After field value SHOULD NOT reference a point in time earlier than the end of the effective window". - Laravel 12.64 mặc định trả
X-RateLimit-Limit,X-RateLimit-Remaining, và khi 429 thêmRetry-After+X-RateLimit-Reset(lab bài 1). Muốn body JSON có tên limiter thì dùng->response()— callback nhận đúng mảng header đó (theo docs và sourcebuildException()):
RateLimiter::for('partner', fn (Request $r) => Limit::perMinute($r->partner()->plan->rpm)
->by('partner:' . $r->partner()->id)
->response(fn (Request $r, array $headers) => response()->json([
'error' => 'rate_limited', 'limiter' => 'partner', 'retry_after' => $headers['Retry-After'] ?? null,
], 429, $headers + ['Cache-Control' => 'no-store'])));
nginx: luôn limit_req_status 429; (mặc định 503); nginx không tự gửi Retry-After — thêm bằng error_page 429 trỏ tới một
location trả header đó (chưa dựng lab cho phần này).
3.2 Phía client: retry thế nào — lab 1 000 client
Server: fixed window 100 request/giây căn theo giây đồng hồ; 429 kèm Retry-After = số giây (nguyên) tới đầu cửa sổ sau. 1 000 client bắt đầu cùng lúc (như sau một
sự cố, một lần deploy, một cron lúc 9h00), mỗi client cần đúng 1 request thành công. Năm chiến lược:
retry ngay, không chờ | tổng request 68675 (×68.7) | xong hết sau 9.4s | đỉnh 1434 request/100ms
chờ đúng Retry-After | tổng request 5201 (×5.2) | xong hết sau 9.4s | đỉnh 441 request/100ms
0–3s, mỗi ô 100ms: 0 140 413 290 142 0 0 0 0 0 0 24 440 263 73 0 0 0 0 0 0 13 441 272 0 0 0 0 0 0
backoff mũ 100ms·2^n, không jitter | tổng request 7176 (×7.2) | xong hết sau 26.7s | đỉnh 679 request/100ms
backoff mũ + full jitter | tổng request 6486 (×6.5) | xong hết sau 11.6s | đỉnh 747 request/100ms
Retry-After + jitter 0–1s | tổng request 3949 (×3.9) | xong hết sau 10.9s | đỉnh 303 request/100ms
0–3s, mỗi ô 100ms: 1 259 303 299 25 0 0 0 0 0 0 116 40 61 93 79 89 73 76 79 56 87 34 27 16 27 29 40 39 67
Retry-After là "ngoan" nhưng tạo đàn đông: mọi client bị chặn trong giây 0 nhận Retry-After: 1, cùng quay lại ở giây 1,1–1,4,
tranh 100 chỗ, phần còn lại nhận Retry-After: 1 tiếp. Cộng thêm jitter 0–1 giây, sau đợt đầu tải rải đều, tổng số request giảm từ 5 201 xuống 3 949.- Retry ngay: ×68,7 tải — mỗi request bị chặn sinh request mới ngay lập tức; server tốn tài nguyên trả 68 000 cái 429.
- Backoff mũ không jitter: 1 000 client bắt đầu cùng lúc thì cùng chờ 100, 200, 400ms… — vẫn đồng bộ, còn chậm nhất (26,7 giây) vì khoảng chờ dài dần trong khi server đang rảnh giữa các đợt.
- Full jitter (
random(0, min(cap, base·2ⁿ))): rải đều tốt nhưng không biết server muốn chờ bao lâu, đợt đầu vẫn dồn (747). - Retry-After + jitter: tốt nhất ở cả tổng request lẫn đỉnh — dùng thông tin server cho, rồi rải ra. Đây là mặc định nên dùng khi server có gửi
Retry-After; không có thì backoff mũ + full jitter.
AWS nhắc thêm một chi tiết vận hành: jitter cho việc định kỳ (cron, đồng bộ) nên tất định theo host — "we use a consistent method that produces the same number every time on the same host" — để sự cố lặp lại theo mẫu, dễ tìm nguyên nhân hơn.
3.3 App Laravel gọi API đối tác: Http::retry() làm gì thật?
API vận chuyển giả trả 429 + Retry-After: 2 cho tới khi đủ 2 giây kể từ lần gọi đầu; API tạo đơn giả luôn trả 422 (dữ liệu sai). Laravel 12.64:
1. Http::retry(3, 100) → ném RequestException, HTTP 429 | đối tác nhận 3 request, cách nhau 147ms → 103ms
2. Http::retry(3, 100, throw: false) → HTTP 429 | đối tác nhận 3 request, cách nhau 104ms → 106ms
3. retry đọc Retry-After + jitter, chỉ retry 429/5xx/mạng → HTTP 200 | đối tác nhận 2 request, cách nhau 2364ms
4. Http::retry(3, 100)->post() vào API trả 422 → ném RequestException, HTTP 422 | đối tác nhận 3 request, cách nhau 107ms → 109ms
5. như 4 nhưng có điều kiện when (chỉ 429/5xx/mạng) → ném RequestException, HTTP 422 | đối tác nhận 1 request
- Mặc định không đọc
Retry-After(dòng 1–2): 3 lần trong ~250ms, đối tác thấy một client "hư" — gặp đối tác như Discord là tiến gần ngưỡng bị cấm (10 000 request lỗi trong 10 phút). - Mặc định retry mọi response không thành công, kể cả 4xx (dòng 4): một POST tạo đơn bị 422 được gửi lại 3 lần. Với API không có idempotency key, retry POST có thể tạo 3 đơn — xem bài idempotency key.
Http::retry(
3,
fn (int $attempt, Throwable $e) => $e instanceof RequestException && $e->response->status() === 429
? (int) $e->response->header('Retry-After') * 1000 + random_int(0, 500) // tôn trọng Retry-After, cộng jitter
: 200 * 2 ** $attempt, // còn lại: backoff mũ
fn (Throwable $e) => $e instanceof ConnectionException
|| ($e instanceof RequestException && in_array($e->response->status(), [429, 502, 503, 504], true)),
)->get($url);
Bẫy của chính đoạn code trên: (1) Retry-After có thể là HTTP-date chứ không phải số — (int) của một ngày là 0 → retry ngay; parse cả hai
dạng nếu đối tác dùng ngày; (2) chờ trong request web giữ worker php-fpm — Retry-After: 60 là 60 giây worker đứng; gọi đối tác từ queue job và
$this->release($retryAfter) thay vì sleep; (3) nhiều worker queue cùng gọi một đối tác thì cần limiter dùng chung phía mình — job middleware
RateLimited trên cache dùng chung (bài scale queue worker: cache riêng từng VM là giới hạn ×N).
3.4 Retry nhân qua nhiều tầng, và ngân sách retry
- Ngân sách retry (Google SRE): mỗi request tối đa 3 lần thử; mỗi client giữ retry dưới 10% tổng request — vượt thì không retry nữa, trả lỗi luôn.
- Token bucket cho chính retry (AWS SDK): "limiting retries locally using a token bucket. This allows all calls to retry as long as there are tokens, and then retry at a fixed rate when the tokens are exhausted. AWS added this behavior to the AWS SDK in 2016." — rate limit áp ngược lên chính client.
- Client tự hạ tải (adaptive throttling, Google SRE): client theo dõi số request đã gửi (
requests) và số được backend nhận (accepts) trong 2 phút; khirequestsvượt K lầnaccepts(mặc định K = 2) thì tự từ chối một phần request ngay tại client với xác suấtmax(0, (requests − K·accepts) / (requests + 1))— backend khỏi tốn tài nguyên trả lỗi. Chưa dựng lab cho cơ chế này.
3.5 Ra mắt limit mới: shadow mode trước
Stripe: "Dark launch each rate limiter to watch the traffic they would block". nginx có sẵn từ 1.17.1 — limit_req_dry_run on; tính như thật, không chặn, và
biến $limit_req_status (từ 1.17.6) ghi kết quả vào access log. Lab, 10 request cùng lúc vào location dry run:
limit_req_dry_run on — 10 request cùng lúc: 200×10
1790925671.652 "/dryrun" client=- 200 PASSED
1790925671.652 "/dryrun" client=- 200 REJECTED_DRY_RUN
1790925671.652 "/dryrun" client=- 200 REJECTED_DRY_RUN
error log: 9 dòng "limiting requests, dry run, excess: … by zone …"
flowchart LR S["1. Shadow mode
tính, không chặn,
ghi log ai sẽ bị chặn"] --> A["2. Phân tích 1–2 tuần
ai bị chặn? khách thật?
đối tác? bot?"] A -->|"chặn nhầm"| F["Sửa key / con số
/ allowlist"] F --> S A -->|"ổn"| P["3. Bật thật cho 1 phần
một route, một tenant,
hoặc % traffic"] P --> M["4. Theo dõi 429,
ticket hỗ trợ,
chuyển đổi đơn hàng"] M -->|"có vấn đề"| K["Kill switch:
quay về shadow"] M -->|"ổn"| ALL["5. Bật toàn bộ"]
Ở tầng app: một middleware tính như thật (cùng key, cùng thuật toán) nhưng chỉ ghi log + metric khi "lẽ ra chặn", bật/tắt bằng biến cấu hình. Envoy ratelimit có sẵn
shadow_mode ("whatever the outcome of the evaluation of the rule, the end-result will always be 'OK'") và thống kê riêng những quyết định bị ghi đè. Chú ý: shadow
mode phải chạy trên traffic thật đủ lâu để gặp cả giờ cao điểm, ngày khuyến mãi, cron của đối tác.
3.6 Giám sát
| Tín hiệu | Vì sao | Ngưỡng cảnh báo gợi ý |
|---|---|---|
| 429 theo limiter × route × loại key | Biết tầng nào đang chặn, chặn ai | Tăng đột ngột so với cùng giờ tuần trước |
| Top key bị chặn, số key khác nhau mỗi phút | Phân biệt 1 khách quá tay với một đợt xoay IP | Số key mới tăng vọt |
| Near-limit (key đang dùng > 80% hạn mức) | Báo trước cho đối tác sắp chạm trần; Envoy ratelimit mặc định 0,8 | Đối tác lớn chạm 80% liên tục |
| Số lần fail-open của limiter | Limiter tắt mà không ai biết (bài 3: lỗi cấu hình bị nuốt) | > 0 trong 5 phút |
| Độ trễ p99 của lệnh limiter, lỗi Redis | Limiter thành điểm chậm của mọi request | p99 > vài ms |
| Status 499 ở nginx (client đóng trước khi có response) | Dấu hiệu sớm của việc làm phí (bài 4) | Tăng cùng lúc với độ trễ |
| Tổng request / số request gốc | Đo mức khuếch đại do retry | > 1,1 |
nginx ghi mỗi lần chặn/giữ vào error log (limiting requests, excess: … by zone "…", delaying request, …); mức log chỉnh bằng
limit_req_log_level — docs: mức cho việc giữ request thấp hơn mức cho việc từ chối một bậc. Đừng để error log thành đường duy nhất thấy 429: tính metric từ
access log ($status, $limit_req_status).
3.7 Sự cố lúc 3 giờ sáng
| Tình huống | Việc đầu tiên | Sau đó |
|---|---|---|
| Đối tác báo "toàn 429" | Dashboard theo API key: chạm hạn mức gói, hay limiter nào khác? | Allowlist tạm có hạn; nếu đúng gói thì báo nâng gói/giãn lịch đồng bộ |
| 429 tăng vọt sau deploy | Đổi gì? key, con số, thứ tự limit, trusted proxy (bài 5)? | Quay cấu hình limit về bản cũ (kill switch), điều tra sau |
| Đang bị tấn công | Siết ở tầng rẻ nhất: CDN/WAF, nginx (không phụ thuộc Redis) | Rà route bị nhắm, thêm limit theo hành động |
| Redis của limiter chết | Xác nhận limiter đang fail-open (không phải 500), cảnh báo đã kêu | Theo dõi traffic bất thường; bật limit tạm ở nginx nếu cần |
| Bão retry | Xem tổng/gốc; khách nào retry dồn | Tăng Retry-After, chặn hẳn client hư theo key, liên hệ đối tác |
4. Đảm bảo và giới hạn
Retry-Afterchỉ là lời khuyên. Client có thể bỏ qua — nên server cần bậc thang xử lý client hư: Discord chặn IP tạm thời ở Cloudflare khi quá 10 000 request lỗi (401, 403, 429) trong 10 phút; GitHub cảnh báo có thể cấm integration.- Độ chính xác của header bị giới hạn bởi limiter: nhiều server, race, thuật toán (bài 2, 3) — "còn 3 lượt" là ước lượng.
- Jitter giảm đỉnh, không giảm việc phải làm: 1 000 client vẫn cần ít nhất 10 giây với server 100/giây (lab: 9,4–11,6 giây ở mọi chiến lược tốt).
- Đơn vị giây nguyên của
Retry-Afterlàm mọi client trong cùng giây nhận cùng số — chính nó sinh đàn đông nếu client không thêm jitter.
5. Failure modes
| Sự cố | Bằng chứng | Chữa |
|---|---|---|
| Client retry ngay khi 429 | Lab: ×68,7 tải | Tôn trọng Retry-After; chặn client hư |
| Đàn đông ở đầu cửa sổ | Lab: đỉnh 441/100ms mỗi đầu giây | Jitter phía client; phía server dùng token bucket/GCRA thay fixed window căn đồng hồ |
| Backoff đồng bộ | Lab: ×7,2 và chậm nhất 26,7 giây | Luôn có jitter |
| Retry nhiều tầng | AWS: 243× | Retry ở một tầng, ngân sách retry |
| Retry 4xx / POST không idempotent | Lab: POST 422 gửi 3 lần | Điều kiện when; idempotency key |
| Ra mắt limit không qua shadow | Khách thật bị chặn, biết qua ticket | Shadow mode + phân tích + bật dần |
| Limiter fail-open không ai biết | Bài 3: 12/12 request qua vì lỗi cấu hình | Cảnh báo theo số lần fail-open |
| CDN cache 429 | (RFC cấm) | Cache-Control: no-store, kiểm cấu hình CDN |
6. Vận hành trong production — checklist
- Trước khi bật: con số có phép tính (bài 4); key đúng (bài 5); thuật toán chạy đúng khi đồng thời (bài 3); 429 +
Retry-After+ body có tên limiter; tài liệu cho đối tác; shadow mode đã chạy qua ít nhất một chu kỳ cao điểm. - Lúc bật: bật dần (route/tenant/phần trăm); có người trực nhìn dashboard; kill switch thử trước.
- Hằng ngày: cảnh báo ở mục 3.6; báo cáo tuần top khách bị chặn và đối tác chạm near-limit (để sales/CS chủ động).
- Định kỳ: load test kiểu mở mỗi release lớn; đo lại phân phối request/user; xem lại trọng số theo chi phí.
- Chi phí: mỗi request thêm 1 lệnh Redis (bài 3: ~0,2ms); log dry run có thể rất nhiều — lấy mẫu nếu traffic lớn.
7. So sánh và ma trận quyết định
| Chiến lược retry (lab) | Tổng request | Xong hết | Đỉnh/100ms | Dùng khi |
|---|---|---|---|---|
| Retry ngay | ×68,7 | 9,4s | 1 434 | Không bao giờ |
| Chờ đúng Retry-After | ×5,2 | 9,4s | 441 | Chỉ khi có ít client |
| Backoff mũ không jitter | ×7,2 | 26,7s | 679 | Không nên |
| Backoff mũ + full jitter | ×6,5 | 11,6s | 747 | Server không gửi Retry-After |
| Retry-After + jitter | ×3,9 | 10,9s | 303 | Mặc định khi có Retry-After |
| Thực thi rate limit ở | Ưu | Nhược | Hợp với |
|---|---|---|---|
| CDN/WAF có rule rate limit | Chặn trước khi tới máy bạn, chịu được khối lượng lớn | Chỉ biết IP/header; ngôn ngữ rule riêng từng hãng (lock-in) | Bot, DDoS, cào thô |
| nginx | Rẻ, nhanh, không phụ thuộc Redis | Theo instance; ít ngữ cảnh | Giới hạn toàn hệ thống, IP dồn dập |
| API gateway / Envoy | Tập trung, có shadow mode, có thống kê | Thêm thành phần, cần đội platform | Nhiều service, API công khai lớn |
| Middleware app | Biết user, gói cước, route | Request đã tốn worker; mỗi service tự làm dễ lệch | Gói cước đối tác, route đắt |
| Nghiệp vụ | Đếm đúng thứ cần đếm (lần sai, thẻ, SMS) | Muộn nhất, đắt nhất | Đăng nhập, OTP, thanh toán |
8. Góc nhìn tech lead: ADR mẫu cho cả series
ADR-0xx: Rate limiting cho shop + API đối tác
Bối cảnh : 1 cụm nginx + php-fpm (capacity đo 76 rps/instance, load test mở ngày …); storefront Livewire;
đăng nhập Fortify; API đối tác gói Free/Pro; Redis riêng cho limiter.
Quyết định :
1. Toàn hệ thống: nginx limit_conn 8/instance (4 worker + 4 chờ), status 429. (bài 4)
2. Storefront: token bucket 120/phút burst 30, key user id | IP (IPv6 /64);
gắn cả route update của Livewire. (bài 4, 5)
3. Đăng nhập: Lua nguyên tử; 5/phút theo email+IP; 10 lần sai/giờ theo email (->after);
CAPTCHA thay vì khoá cứng. (bài 3, 5)
4. API đối tác: GCRA theo API key → tenant, theo gói; quota tháng trong DB. (bài 2, 5)
5. trustProxies: danh sách dải LB + CDN tường minh, không dùng '*'. (bài 5)
6. Limiter fail-open + cảnh báo; timeout Redis 200ms; storefront fail-open,
đăng nhập fallback limiter cục bộ. (bài 3)
7. Header: 429, Retry-After, X-RateLimit-*, body có tên limiter; no-store. (bài 1, 6)
8. Mọi limit mới: shadow ≥ 1 tuần → bật dần → kill switch. (bài 6)
Hệ quả : khách sau CGNAT có thể chạm 120/phút (chấp nhận, theo dõi); đối tác phải xử lý 429
đúng (tài liệu + ví dụ code retry); thêm 1 lệnh Redis/request (~0,2ms).
Đo lường : dashboard 429 theo limiter, near-limit đối tác, fail-open, p99 limiter, tổng/gốc.
Xem lại : mỗi release lớn, trước mùa khuyến mãi, khi nâng Laravel (12 → 13 đổi trustProxies '*').
Thoát : định nghĩa limit ở 1 chỗ, interface allow(key, cost); chuyển sang gateway chỉ đổi nơi thực thi.
- Hợp đồng với đối tác là sản phẩm: trang tài liệu rate limit (giới hạn từng gói, header, ví dụ retry đúng), thông báo trước khi đổi, kênh xin nâng hạn mức.
- Ai trực: người trực cần quyền bật kill switch và allowlist tạm — không phải chờ người viết limiter thức dậy.
9. Anti-patterns và bài học thực tế
- Tin thư viện retry mặc định —
Http::retry(3, 100)bỏ quaRetry-After, retry cả 422 (lab). - Retry ở mọi tầng — 243×.
- Backoff không jitter — vẫn đồng bộ (lab: chậm nhất, ×7,2).
sleep(Retry-After)trong request web — giữ worker php-fpm; dùng queue vàrelease().- Bật limit thẳng production không qua shadow.
- Chỉ nhìn error log nginx để biết ai bị chặn — thiếu metric theo khách.
- Không có bậc thang cho client hư — client bỏ qua 429 mãi mãi.
Nguồn công khai: AWS Builders' Library (Marc Brooker) — 243×, "retries are selfish", jitter cho việc định kỳ, token bucket trong AWS SDK từ 2016; Google SRE ch. 21
— ngân sách retry 3 lần/request và 10%/client, adaptive throttling K = 2; Stripe 2017 — dark launch, kill switch; Discord — ngưỡng 10 000 request lỗi/10 phút; GitHub —
"wait for at least one minute before retrying" khi không có retry-after, và có thể cấm integration.
10. Câu hỏi phụ interviewer hay hỏi tiếp
- Client nên làm gì khi nhận 429?
- Chờ đúng
Retry-Aftercộng jitter, giới hạn số lần, không retry 4xx khác; không cóRetry-Afterthì backoff mũ + full jitter. Lab: ×3,9 so với ×68,7 khi retry ngay. - Vì sao cần jitter khi đã có backoff?
- Backoff không jitter vẫn đồng bộ các client bắt đầu cùng lúc (lab: đỉnh 679, chậm nhất). Jitter phá sự đồng bộ.
- Ra mắt một rate limit mới thế nào để không chặn nhầm khách?
- Shadow mode (nginx
limit_req_dry_run, Envoyshadow_mode, hoặc middleware chỉ log) qua ít nhất một chu kỳ cao điểm → phân tích ai sẽ bị chặn → bật dần → kill switch. - Header nào nên trả?
- 429 +
Retry-Afterbắt buộc;X-RateLimit-*hoặcRateLimit-Policy/RateLimit(draft IETF); body nói limiter nào;no-store. - Giám sát rate limit thế nào?
- 429 theo limiter × route × khách, near-limit, fail-open, độ trễ limiter, tổng/gốc (khuếch đại do retry), 499.
- Retry storm là gì, chặn thế nào?
- Lỗi sinh retry, retry sinh thêm tải. Chặn: retry ở một tầng, ngân sách retry (≤ 10%), token bucket cho retry, jitter, client tự hạ tải khi tỉ lệ thành công thấp.
- App Laravel gọi API đối tác bị 429 thì xử lý sao?
- Gọi từ queue job,
release($retryAfter)thay vì sleep; limiter dùng chung phía mình (RateLimitedtrên Redis);Http::retryvới closure đọcRetry-Aftervà điều kiện chỉ retry 429/5xx/lỗi mạng.
Phụ lục: lab đã chạy
Môi trường: Node 22.23.3 (server limiter chạy process riêng bằng fork, 1 000 client cùng process), Laravel 12.64 + PHP 8.3.33 (mock API đối tác
bằng php -S trong cùng container, --network none), nginx 1.31.3.
cd labs/rate-limit
node/run.sh 06-retry.mjs # 5 chiến lược retry, 1 000 client → out/06-retry.txt
./06-laravel-http.sh # Http::retry với 429 + Retry-After và với 422 → out/06-laravel-http.txt
./05-nginx.sh # có phần limit_req_dry_run + $limit_req_status → out/05-nginx.txt
Chưa verify: nginx gửi Retry-After qua error_page 429; middleware shadow ở tầng app; adaptive throttling phía client (công thức Google SRE); hành vi
CDN với 429; cách các đối tác thật (vận chuyển, thanh toán) trả Retry-After dạng ngày.