AI Fundamentals

Rate Limit, Retry và Exponential Backoff trong LLM API: Thiết kế để 429 không biến thành sự cố dây chuyền

Lỗi 429 không chỉ là chuyện gửi quá nhiều request. Bài viết xây dựng mental model đầy đủ về rate limit, retry, exponential backoff, jitter, timeout và backpressure khi tích hợp LLM API.

Minh họa cho bài Rate Limit, Retry và Exponential Backoff trong LLM API: Thiết kế để 429 không biến thành sự cố dây chuyền
Mục lục bài viết

Hãy tưởng tượng backend của bạn có 200 worker cùng gọi một LLM API. Mọi thứ chạy bình thường cho đến khi lưu lượng tăng đột ngột và provider bắt đầu trả về 429 Too Many Requests.

Cách xử lý đầu tiên nghe rất hợp lý: nếu request thất bại thì thử lại.

Python
while True:
    try:
        return call_llm()
    except RateLimitError:
        continue

Nhưng đây chính là lúc một lỗi có khả năng tự phục hồi có thể biến thành một sự cố lớn hơn.

Nếu cả 200 worker đều retry ngay lập tức, provider vừa từ chối 200 request sẽ nhận thêm 200 request gần như ngay sau đó. Nếu chúng tiếp tục thất bại, hệ thống lại gửi thêm một đợt nữa. Thay vì giảm áp lực để dependency có cơ hội phục hồi, application của chúng ta đang chủ động tăng tải lên đúng dependency đang quá tải.

Vấn đề càng khó hơn với LLM API vì giới hạn không chỉ tồn tại dưới dạng "bao nhiêu request mỗi giây". Provider có thể đồng thời kiểm soát số request, số input token, số output token, tốc độ tăng traffic và thậm chí tốc độ chi tiêu. Vì vậy hai request có thể trông giống nhau ở tầng HTTP nhưng tiêu thụ lượng capacity hoàn toàn khác nhau. OpenAI hiện khuyến nghị exponential backoff khi gặp rate-limit error và lưu ý rằng request thất bại vẫn có thể tiếp tục đóng góp vào giới hạn theo phút; Anthropic sử dụng nhiều limiter theo request và token; Gemini cũng áp dụng nhiều chiều như RPM, TPM, RPD và trong một số tier còn có spend-based limits.

Điều cần thiết vì thế không phải là thêm một decorator @retry rồi coi vấn đề đã được giải quyết.

Một hệ thống LLM đáng tin cậy phải trả lời được ít nhất bốn câu hỏi:

  1. Request này thất bại vì lý do gì?
  2. Lỗi đó có thực sự đáng retry không?
  3. Nếu retry, khi nào mới nên gửi lại?
  4. Bao nhiêu retry là đủ trước khi chúng ta dừng lại?

Bài viết này sẽ xây dựng mental model đó từ đầu, sau đó đi sâu tới exponential backoff, jitter, retry amplification, timeout, retry budget và client-side rate limiting.


1. Rate limit không phải chỉ là một lỗi HTTP

Khi nhìn thấy:

Text
HTTP 429 Too Many Requests

developer rất dễ hình dung provider đang nói:

"Bạn gửi quá nhiều request."

Điều đó đúng, nhưng chưa đủ.

Một cách hiểu tốt hơn là:

Rate limit là hợp đồng phân bổ tài nguyên giữa client và provider.

Provider có một lượng compute hữu hạn cần chia cho rất nhiều workload khác nhau. Rate limiter xác định application của bạn được tiêu thụ tài nguyên với tốc độ nào trong một khoảng thời gian nhất định.

Với một REST API thông thường, số lượng request thường là approximation khá tốt cho lượng tài nguyên tiêu thụ. Nhưng với LLM, hai request HTTP có thể khác nhau rất nhiều.

Text
Request A
"Translate hello to Spanish"
≈ vài chục tokens

Request B
[80.000-token document]
+
"Analyze this document"
+
large output
≈ hàng chục nghìn tokens

Nếu chỉ giới hạn request count, hai workload này được xem là tương đương mặc dù chi phí inference rất khác nhau. Đó là lý do các provider thường cần nhiều dimension của rate limiting thay vì một counter duy nhất. Research ban đầu cũng xác định RPM và token throughput là hai bottleneck độc lập quan trọng trong LLM workload.

Một mental model đơn giản là hình dung application đang đi qua nhiều cửa kiểm soát cùng lúc:

Text
                    ┌──────────────────┐
Request ───────────►│ RPM              │
                    │ requests / time  │
                    └────────┬─────────┘
                             │ pass
                             ▼
                    ┌──────────────────┐
                    │ Token throughput │
                    │ TPM / ITPM /     │
                    │ OTPM             │
                    └────────┬─────────┘
                             │ pass
                             ▼
                    ┌──────────────────┐
                    │ Other quotas     │
                    │ daily / spend /  │
                    │ burst controls   │
                    └────────┬─────────┘
                             │
                             ▼
                          Inference

Request chỉ được xử lý nếu nó không vi phạm limiter đang có hiệu lực.

Những dimension bạn thường gặp

Dimension Đo cái gì? Workload dễ chạm giới hạn
RPM Số request trong một khoảng thời gian Chatbot nhiều user, agent fan-out
TPM Tổng token throughput RAG, document processing
ITPM Input token throughput Long context, large prompts
OTPM Output token throughput Long generation
RPD Request theo ngày Automation quy mô lớn
Spend limit Chi phí trong một cửa sổ thời gian Expensive models, huge contexts

Tên gọi và cách tính cụ thể phụ thuộc provider và model.

Ví dụ, Anthropic hiện đo Messages API bằng RPM, ITPM và OTPM. Documentation của họ cũng nói limiter sử dụng token-bucket và capacity được replenish liên tục thay vì đơn giản reset toàn bộ vào cuối mỗi phút.

Gemini hiện mô tả rate limits phổ biến theo RPM, input TPM và RPD. Ngoài ra một số paid tier còn có spend-based rate limit trên rolling 10-minute window; vượt limit này cũng có thể trả về 429 RESOURCE_EXHAUSTED.

Điều này dẫn tới một implication quan trọng:

💡 429 là symptom, không nhất thiết là root cause.

Khi production có nhiều 429, câu hỏi đầu tiên không nên là "backoff bao nhiêu giây?", mà là:

Limiter nào đang bị chạm?

Nếu RPM đã cạn, giảm concurrency hoặc request rate có thể giải quyết vấn đề. Nếu token throughput mới là bottleneck, việc giữ nguyên traffic nhưng giảm prompt size có thể hiệu quả hơn. Nếu application đã chạm một quota dài hạn hoặc billing constraint, retry đơn thuần thậm chí không có ý nghĩa.


2. Trước khi retry, phải phân loại failure

Retry chỉ hữu ích khi failure có khả năng biến mất nếu chúng ta thử lại sau.

Đây là distinction quan trọng nhất của toàn bộ retry design.

Giả sử server trả:

Text
400 Bad Request

vì JSON schema sai.

Đợi 1 giây rồi gửi lại cùng JSON sẽ không giúp gì.

Đợi 2 giây cũng không.

Đợi 4, 8 hay 16 giây vẫn không.

Failure nằm trong request và không thay đổi theo thời gian.

Ngược lại, nếu provider tạm thời hết capacity thì cùng một request có thể thành công vài giây sau. Đây mới là failure mà retry được thiết kế để xử lý.

Retryability matrix

Failure Thường retry? Vì sao
Network connection reset Có Có thể chỉ là lỗi mạng tạm thời
Timeout Có điều kiện Request có thể thành công nếu thử lại
408 Request Timeout Thường có Failure thường mang tính transient
429 Rate Limit Có Capacity có thể phục hồi theo thời gian
500 Thường có Server-side transient failure
502/503/504 Thường có Gateway/service availability
400 Bad Request Không Request sai vẫn sẽ tiếp tục sai
401 Unauthorized Không Credentials cần được sửa
403 Forbidden Thường không Permission không tự thay đổi
Validation/schema failure Không ở transport layer Cần sửa request

Gemini hiện khuyến nghị retry các transient errors như 429, 408 và 5xx, đồng thời tránh retry các client errors như 400 hoặc 403.

Anthropic cũng phân biệt rất rõ hai failure thường bị developer gom chung thành "Claude không gọi được":

Text
429 rate_limit_error

nghĩa là account/workload đã chạm một rate limit, trong khi:

Text
529 overloaded_error

nghĩa là API đang tạm thời overloaded.

Hai lỗi đều có thể transient và đều có thể dẫn đến retry, nhưng chúng không mang cùng thông tin operational. Một lỗi cho thấy pressure liên quan đến traffic của client; lỗi còn lại phản ánh capacity của service rộng hơn.

Đây là lý do production code không nên có policy kiểu:

Python
except Exception:
    retry()

Mà nên có một classifier rõ ràng:

Text
Failure
   │
   ├── Permanent ─────────► Fail fast
   │
   └── Transient
          │
          ├── Server supplied retry hint?
          │        │
          │        └── Yes ─► respect it
          │
          └── No ─────────► backoff + jitter

Classification phải xảy ra trước backoff.

Exponential backoff không thể biến một permanent failure thành transient failure.


3. Vì sao immediate retry và fixed retry đều có vấn đề?

Hãy bắt đầu bằng cách thử ba chiến lược.

Cách 1: retry ngay lập tức

Text
failure
retry
failure
retry
failure
retry
...

Đây gần như luôn là lựa chọn tệ khi failure đến từ contention hoặc overload.

OpenAI hiện lưu ý rằng các request không thành công vẫn có thể góp vào per-minute rate limit, vì vậy gửi lại liên tục không giúp client thoát khỏi limiter.

Nhưng chúng ta có thể sửa bằng cách thêm delay:

Python
time.sleep(1)

Bây giờ mỗi worker chờ một giây.

Vấn đề đã hết chưa?

Chưa.

Cách 2: fixed delay

Giả sử 1.000 worker cùng nhận 429 tại thời điểm 12:00:00.

Tất cả đều thực hiện:

Text
sleep(1)

Chuyện gì xảy ra?

Text
12:00:00    1000 requests fail
                │
                │ sleep 1s
                ▼
12:00:01    1000 retries

Nếu vẫn thất bại:

Text
12:00:02    1000 retries
12:00:03    1000 retries
...

Application đã giảm request rate trung bình, nhưng các client vẫn đồng bộ hóa với nhau.

Đây là dạng thundering herd: rất nhiều worker cùng thức dậy và tranh giành cùng một tài nguyên vào cùng một thời điểm.

Một cải tiến tự nhiên là tăng delay sau mỗi failure.


4. Exponential backoff: giảm tốc độ retry theo cấp số nhân

Thay vì luôn đợi 1 giây, chúng ta tăng thời gian chờ theo số lần thất bại:

Text
attempt 1 → 1s
attempt 2 → 2s
attempt 3 → 4s
attempt 4 → 8s
attempt 5 → 16s
...

Công thức cơ bản:

delayn=base×2n

Trong production, delay thường được cap:

delayn=min⁡(cap, base×2n)

Ví dụ với:

Text
base = 1 second
cap  = 30 seconds

ta có:

Text
1 → 1s
2 → 2s
3 → 4s
4 → 8s
5 → 16s
6 → 30s
7 → 30s
8 → 30s

Research cung cấp cũng mô tả capped exponential backoff như cơ chế trung tâm để giảm request pressure sau mỗi failure.

Điểm quan trọng không nằm ở việc "2 mũ n" có gì đặc biệt.

Nó nằm ở feedback loop.

Nếu dependency chỉ bị lỗi trong một khoảng thời gian rất ngắn, retry đầu tiên vẫn xảy ra tương đối nhanh. Nhưng nếu failure kéo dài, client tự động giảm tốc độ gửi request ngày càng mạnh.

Text
Transient 1-second outage
→ retry nhanh
→ UX ít bị ảnh hưởng

Long outage
→ delay tăng mạnh
→ server được bảo vệ

Đó là một trade-off tốt.

Nhưng pure exponential backoff vẫn còn một lỗi rất lớn.

Backoff làm client chậm lại nhưng không làm chúng khác nhau

Quay lại 1.000 worker ban đầu.

Nếu mọi worker đều chạy:

Text
1s → 2s → 4s → 8s

thì timeline trở thành:

Text
12:00:00 ───── 1000 failures
12:00:01 ───── 1000 retries
12:00:03 ───── 1000 retries
12:00:07 ───── 1000 retries
12:00:15 ───── 1000 retries

Các spike thưa hơn.

Nhưng chúng vẫn là spike.

Backoff giải quyết frequency.

Nó chưa giải quyết synchronization.


5. Jitter: randomness là một phần của reliability

Đây là lúc một ý tưởng hơi ngược trực giác xuất hiện:

Muốn hệ thống distributed hoạt động ổn định hơn, hãy cố tình thêm randomness.

AWS đã mô phỏng chính vấn đề contention này và cho thấy exponential backoff không jitter vẫn tạo ra các cluster request. Khi thêm jitter, request được trải đều hơn, giảm cả lượng client work lẫn server load.

Ý tưởng rất đơn giản.

Thay vì:

Text
retry after exactly 8 seconds

hãy sử dụng:

Text
retry sometime within a window up to 8 seconds

Một nhóm worker khi đó có thể phân bố như sau:

Text
Worker A ────── 0.8s
Worker B ───────────── 2.1s
Worker C ───────────────── 3.4s
Worker D ─────────────────────── 5.7s
Worker E ───────────────────────────── 7.6s

Thay vì:

Text
1000 clients
       │
       ▼
       ███████████████
       one giant spike

ta muốn:

Text
1000 clients
       │
       ▼
 ▂ ▃ ▅ ▃ ▆ ▄ ▅ ▃ ▅ ▂ ▄
 spread across time

Research ban đầu cũng đi đến chính điểm này: pure exponential backoff làm các client giảm frequency nhưng có thể giữ nguyên synchronization, vì vậy jitter cần được thêm vào để phá tương quan giữa các client.

🔬 Deep Dive — Full Jitter, Equal Jitter và Decorrelated Jitter

Không phải mọi jitter đều giống nhau.

AWS mô tả ba chiến lược phổ biến.

Full Jitter

Đầu tiên tính exponential ceiling:

capn=min⁡(cap, base×2n)

sau đó chọn ngẫu nhiên:

sleep=random(0, capn)

Ví dụ attempt hiện tại có ceiling 8 giây:

Text
possible sleep:
0.2s
1.7s
4.1s
6.8s
7.9s
...

Đây là Full Jitter.

Ưu điểm quan trọng là toàn bộ retry population được trải trên một window rộng.


Equal Jitter

Một cách khác là đảm bảo client luôn ngủ ít nhất một nửa backoff:

sleep=capn2+random(0,capn2)

Nếu ceiling là 8 giây:

Text
sleep ∈ [4s, 8s]

Intuition rất hấp dẫn: "đừng bao giờ retry quá sớm."

Nhưng đổi lại population chỉ được phân phối trên một window nhỏ hơn.

Trong simulation của AWS, Equal Jitter nhìn chung kém Full Jitter hơn về thời gian hoàn thành trong scenario được thử nghiệm.


Decorrelated Jitter

Một approach khác sử dụng delay trước đó để quyết định range mới:

sleep=min⁡(cap, random(base, previoussleep×3))

Delay khi đó không còn được quyết định hoàn toàn bởi attempt number.

AWS nhận thấy lựa chọn giữa Full và Decorrelated Jitter phụ thuộc trade-off: Full Jitter tạo ít client work hơn trong simulation, trong khi Decorrelated Jitter có thể đạt completion time khác biệt; cả hai đều tốt hơn đáng kể so với no-jitter backoff trong contention scenario được mô phỏng.

Strategy Randomness Synchronization Client work
Fixed retry Không Rất cao Cao
Exponential only Không Vẫn cao Cao hơn cần thiết
Equal Jitter Có Giảm Tốt hơn
Full Jitter Có mạnh Giảm mạnh Rất tốt
Decorrelated Jitter Có + stateful Giảm mạnh Trade-off khác Full

Điều đáng chú ý là đây không chỉ là lý thuyết.

Tenacity hiện mô tả wait_random_exponential() chính là chiến lược có exponentially widening random window tương ứng với Full Jitter, và khuyến nghị nó cho contention giữa nhiều process cùng chia sẻ resource.


6. Một retry policy tốt cần cả deadline lẫn budget

Cho đến đây chúng ta có:

Text
Transient failure
       │
       ▼
Exponential backoff
       │
       ▼
Jitter

Nhưng vẫn còn thiếu một câu hỏi:

Retry tới bao giờ?

Một policy như:

Python
while failure:
    retry()

không phải fault tolerance.

Đó là một infinite loop được ngụy trang thành resilience.

Một production retry policy thường cần ít nhất:

Text
Maximum attempts
+
Maximum elapsed time / deadline
+
Retryable error filter
+
Backoff strategy

Ví dụ:

Text
maximum attempts = 5
overall deadline = 30 seconds
maximum sleep    = 8 seconds

Bây giờ retry là một ngân sách hữu hạn.

Text
Request
 │
 ├─ Attempt 1 ─ fail
 │
 ├─ wait
 │
 ├─ Attempt 2 ─ fail
 │
 ├─ wait
 │
 ├─ Attempt 3 ─ fail
 │
 └─ deadline exceeded
        │
        ▼
      Fail

Điều này cực kỳ quan trọng với user-facing request.

Nếu HTTP request phía ngoài chỉ có deadline 15 giây nhưng LLM wrapper sẵn sàng retry trong 60 giây, application đã có một policy mâu thuẫn: upstream client đã bỏ đi từ lâu trong khi backend vẫn tiếp tục tiêu thụ resource.

Timeout và retry là một hệ thống, không phải hai setting độc lập

Timeout trả lời:

"Một attempt được phép chờ bao lâu?"

Retry budget trả lời:

"Toàn bộ operation được phép cố gắng bao lâu?"

Hai khái niệm phải được thiết kế cùng nhau.

Text
Total request deadline: 20s

Attempt 1:
  5s timeout
      ↓ failure

Jitter:
  1.2s

Attempt 2:
  5s timeout
      ↓ failure

Jitter:
  3.4s

Attempt 3:
  only 5.4s budget remains

Nếu code bỏ qua parent deadline, retry logic có thể tự kéo operation vượt xa latency SLO của application.

🏗️ Production Note

Một retry policy không nên chỉ có max_retries.

Hãy nghĩ theo:

Text
retry budget = attempts + elapsed time + caller deadline

Số attempt chỉ là một trong các guardrail.


7. Retry ở nhiều layer có thể khuếch đại traffic theo cấp số nhân

Giả sử architecture như sau:

Text
Client
  │
  ▼
API Service
  │
  ▼
Agent Service
  │
  ▼
LLM Gateway
  │
  ▼
Provider SDK
  │
  ▼
LLM API

Bây giờ developer ở mỗi layer đều muốn "làm hệ thống reliable hơn".

Vì vậy:

Text
API Service       retry 3 times
Agent Service     retry 3 times
Gateway           retry 3 times
SDK               retry 3 times

Một dependency failure ở đáy stack có thể bị nhân lên rất nhanh.

AWS đưa ra ví dụ một service stack sâu năm lớp, mỗi lớp retry ba lần: failure tại dependency cuối có thể tạo ra tới:

35=243

lần gọi xuống database cho một request ban đầu.

Trong LLM stack, vấn đề còn dễ bị che giấu vì retry có thể tồn tại ở rất nhiều nơi:

Text
Application retry
       ×
Agent framework retry
       ×
Gateway retry
       ×
Provider SDK retry
       ×
HTTP transport retry

Điều này đặc biệt đáng chú ý vì một số SDK đã có retry sẵn.

Anthropic hiện cho biết official SDK tự động retry transient failures như connection errors, rate limits và 5xx bằng exponential backoff hai lần theo mặc định, đồng thời tôn trọng retry-after khi header này có mặt.

Gemini documentation hiện cũng cho biết official client SDK có automatic retry cho transient errors; Python SDK được mô tả là retry tối đa bốn lần với delay tăng dần trong cấu hình mặc định hiện tại.

Vì vậy code này:

Python
@retry(...)
def ask_model():
    return provider_sdk.generate(...)

có thể không có nghĩa là:

Text
maximum attempts = 4

mà có thể là:

Text
application attempts
×
SDK internal attempts

Đó là lý do trước khi thêm retry wrapper, hãy kiểm tra retry behavior của layer bên dưới.

⚠️ Retry phải có owner.

Một architecture dễ reasoning hơn là xác định rõ layer nào chịu trách nhiệm cho transport retry và layer nào không.

Text
Application
    │
    │ no transport retries
    ▼
LLM Gateway / Client Layer
    │
    │ owns retry policy
    ▼
Provider

Không phải mọi hệ thống đều cần retry tại đúng một layer tuyệt đối, nhưng nếu có nhiều layer, budget phải được phối hợp thay vì mỗi layer tự hành động độc lập.


8. Server đã nói khi nào retry thì đừng đoán

Exponential backoff + jitter là một default rất tốt khi client không biết bao lâu dependency mới sẵn sàng.

Nhưng đôi khi server biết rõ hơn.

Ví dụ:

http
HTTP/1.1 429 Too Many Requests
Retry-After: 7

Server đang nói:

"Đừng gửi lại trước khoảng thời gian này."

Anthropic hiện trả retry-after khi rate limit bị vượt; documentation mô tả giá trị này là số giây cần đợi và nói rằng retry sớm hơn sẽ thất bại.

Trong trường hợp này, random backoff 1.3 giây là quyết định tệ:

Text
Server: wait 7 seconds
Client: I'll try after 1.3 seconds

Một policy tốt hơn là:

Text
Transient failure
       │
       ▼
Is Retry-After available?
       │
   ┌───┴───┐
  yes      no
   │        │
   ▼        ▼
respect    exponential
server     backoff
hint       + jitter

Pseudo-code:

Python
def compute_wait(error, attempt):
    retry_after = parse_retry_after(error.response)

    if retry_after is not None:
        return retry_after

    ceiling = min(30, 2 ** attempt)
    return random.uniform(0, ceiling)

Trong hệ thống thực tế, bạn còn cần cap hợp lý để một malicious hoặc malformed response không làm worker ngủ vô hạn, đồng thời phải so sánh delay với remaining request deadline.

Ví dụ:

Python
wait = min(
    provider_retry_after,
    remaining_deadline,
    configured_max_wait,
)

Điểm cốt lõi là thứ tự ưu tiên:

Text
provider signal
       ↓
local policy / deadline
       ↓
fallback jitter algorithm

chứ không phải:

Text
ignore provider
       ↓
random sleep

9. Triển khai retry bằng Python mà không biến decorator thành magic

Tenacity là một retry library phổ biến trong Python và hỗ trợ trực tiếp stop conditions, exception filters cũng như wait_random_exponential. Documentation hiện mô tả strategy này là Full Jitter cho distributed contention.

Một wrapper tối giản có thể trông như sau:

Python
from tenacity import (
    retry,
    retry_if_exception_type,
    stop_after_attempt,
    wait_random_exponential,
)


class TransientLLMError(Exception):
    pass


@retry(
    retry=retry_if_exception_type(TransientLLMError),
    wait=wait_random_exponential(
        multiplier=1,
        max=30,
    ),
    stop=stop_after_attempt(5),
    reraise=True,
)
def call_model():
    return send_request()

Có bốn quyết định quan trọng trong đoạn code này.

1. Retry filter

Python
retry_if_exception_type(TransientLLMError)

Policy không retry mọi exception.

Application phải chuyển HTTP/network errors thành taxonomy rõ ràng:

Text
TransientLLMError
PermanentLLMError
AuthenticationError
QuotaError
...

Điều này giúp retry logic không cần biết hàng chục chi tiết provider.


2. Full Jitter backoff

Python
wait_random_exponential(
    multiplier=1,
    max=30,
)

Retry window tăng theo cấp số nhân nhưng actual sleep được randomize.


3. Bounded attempts

Python
stop_after_attempt(5)

Không có infinite retry.

Trong production, bạn có thể muốn kết hợp attempt limit với elapsed-time budget thay vì chỉ dựa vào một con số cố định.


4. Preserve original failure

Python
reraise=True

Khi retry budget cạn, application layer vẫn nhận được error thực và có thể quyết định:

Text
return fallback
queue for later
surface degraded response
mark job failed
trigger circuit breaker

Thay vì coi retry library là nơi "nuốt" failure.

Nhưng decorator vẫn chưa đủ

Đoạn code trên chưa xử lý:

  • Retry-After;
  • request deadlines;
  • existing SDK retries;
  • idempotency;
  • concurrency;
  • token rate limiting;
  • observability.

Vì vậy production design thường nên có một LLM client abstraction thay vì rải @retry khắp business code.

Text
Business Logic
      │
      ▼
┌────────────────────────┐
│      LLM Client        │
│                        │
│ timeout                │
│ retry classification   │
│ retry budget           │
│ jitter                 │
│ rate limiter           │
│ metrics / tracing      │
└───────────┬────────────┘
            │
            ▼
       Provider SDK

Business code khi đó chỉ cần nói:

Python
result = llm_client.generate(request)

Toàn bộ network resilience được quản lý ở một boundary rõ ràng.


10. Retry giải quyết failure sau khi xảy ra; rate limiter cố ngăn failure xảy ra

Đây là distinction rất quan trọng.

Retry là reactive:

Text
send
 ↓
429
 ↓
wait
 ↓
retry

Client-side rate limiting là proactive:

Text
request ready
     │
     ▼
local limiter
     │
     ├─ capacity available ─► send
     │
     └─ no capacity ────────► wait locally

Nếu application biết provider cho phép một throughput nhất định, không có lý do gì phải cố tình gửi vượt xa nó rồi dùng 429 làm flow-control mechanism.

Anthropic documentation hiện xác nhận provider sử dụng token-bucket rate limiting, tức capacity được replenish dần theo thời gian.

Mental model token bucket rất trực quan.

Text
             refill
               │
               ▼
        ┌─────────────┐
        │ ● ● ● ● ●   │
        │ token bucket│
        └──────┬──────┘
               │ consume
               ▼
             Request

Mỗi request muốn chạy phải lấy capacity từ bucket.

Nếu bucket hết:

Text
Request A ─┐
Request B ─┼─► WAIT
Request C ─┘

thay vì:

Text
Request A ─► Provider ─► 429
Request B ─► Provider ─► 429
Request C ─► Provider ─► 429

Đây là backpressure: application làm chậm producer trước khi dependency phải từ chối traffic.

Concurrency limit không giống rate limit

Giả sử provider cho phép throughput lớn nhưng mỗi call mất 20 giây.

Application chạy:

Text
500 requests
all at once

Ngay cả khi RPM chưa vượt ngưỡng, backend của chính bạn có thể gặp vấn đề vì có 500 in-flight tasks cùng giữ:

  • connections;
  • memory;
  • queues;
  • request state.

Do đó production client thường cần cả:

Text
rate limit
+
concurrency limit

Ví dụ:

Text
Incoming jobs
      │
      ▼
┌───────────────┐
│ Concurrency   │  max 20 in-flight
│ Semaphore     │
└───────┬───────┘
        ▼
┌───────────────┐
│ Rate Limiter  │  smooth traffic
└───────┬───────┘
        ▼
     LLM API

Hai control loop giải quyết hai vấn đề khác nhau:

Text
Concurrency
→ bao nhiêu operation đang chạy đồng thời?

Rate
→ operation được bắt đầu nhanh tới mức nào?

Đây là bước chuyển quan trọng từ "handle 429" sang capacity engineering.


11. Retry không an toàn nếu operation có side effect

Giả sử application gọi:

Text
LLM
 ↓
tool call
 ↓
charge_customer()

Request tới một downstream service timeout.

Client không biết chuyện gì đã xảy ra:

Text
Possibility A:
request chưa đến server

Possibility B:
server nhận request nhưng chưa chạy

Possibility C:
server đã charge thành công,
response bị mất trên network

Nếu retry blind:

Text
charge_customer()

lần thứ hai, side effect có thể xảy ra hai lần.

Đây là bài toán idempotency.

Một operation idempotent cho phép cùng một logical request được gửi lại mà không tạo thêm side effect ngoài ý muốn. AWS coi idempotency là nền tảng quan trọng để làm retry an toàn trong distributed systems.

Ví dụ:

Text
request_id = "order-9832-payment"

Attempt 1
POST payment
Idempotency-Key: order-9832-payment

response lost

Attempt 2
POST payment
Idempotency-Key: order-9832-payment

Server có thể nhận biết:

Text
"I have already processed this logical operation."

và không thực thi side effect lần nữa.

Điều này đặc biệt quan trọng trong agent systems, bởi retry có thể xảy ra không chỉ ở HTTP transport mà còn ở cấp orchestration.

Ví dụ:

Text
LLM generates tool call
      │
      ▼
Tool executes successfully
      │
      ▼
response path fails
      │
      ▼
Agent retries
      │
      ▼
Tool executes again?

Một retry policy tốt vì thế phải phân biệt:

Text
Retrying inference

và:

Text
Retrying an operation with external side effects

Hai thứ không thể được đối xử giống nhau.


12. Khi backoff không còn là lời giải

Một hệ thống liên tục sống nhờ retry có thể vẫn đang có architecture sai.

Giả sử metrics cho thấy:

Text
requests = 100.000/day

first-attempt success = 65%
after retries         = 99.5%

99.5% nghe có vẻ tốt.

Nhưng 35% request phải retry nghĩa là dependency thường xuyên bị gọi vượt khả năng phục vụ hoặc application traffic không phù hợp với capacity contract.

Backoff lúc này chỉ đang che symptom.

Nếu bottleneck là RPM

Bạn có thể:

  • giảm fan-out;
  • giới hạn concurrency;
  • queue workload;
  • gom các task không realtime sang batch processing.

Anthropic Message Batches hiện có quota riêng cho batch workload và được thiết kế cho xử lý bất đồng bộ khối lượng lớn; provider hiện công bố mức giá batch thấp hơn 50% so với standard processing.

Gemini cũng hiện tách Batch API rate limits khỏi non-batch API traffic.

Điều này phản ánh một nguyên lý architecture rộng hơn:

Nếu workload không cần realtime, đừng bắt nó tranh capacity với realtime traffic.

Nếu bottleneck là token throughput

Hãy nhìn vào prompt shape.

Text
100 requests × 2k tokens

khác rất nhiều với:

Text
100 requests × 100k tokens

Các đòn bẩy có thể là:

Text
smaller context
better retrieval
deduplication
prompt caching
request shaping

Anthropic hiện không tính cache_read_input_tokens vào ITPM đối với phần lớn model được tài liệu của họ liệt kê, vì vậy caching repeated context có thể tăng effective throughput đáng kể.

Đây là ví dụ rất rõ cho việc:

Text
rate-limit problem

không nhất thiết được giải quyết bằng:

Text
retry algorithm

Đôi khi lời giải nằm ở việc giảm lượng tài nguyên cần dùng cho mỗi successful request.

Nếu bottleneck là traffic burst

Queue hoặc local token bucket có thể chuyển:

Text
████████████████
1000 req instantly

thành:

Text
██ ██ ██ ██ ██ ██ ██ ██
controlled flow over time

Không cần chờ provider trả 429 trước mới bắt đầu điều tiết.


13. Một production architecture nên trông như thế nào?

Ghép các mảnh lại, một LLM integration trưởng thành có thể được hình dung như sau:

Text
                 ┌─────────────────┐
                 │ Application     │
                 └────────┬────────┘
                          │
                          ▼
                 ┌─────────────────┐
                 │ Queue /         │
                 │ Concurrency     │
                 │ Control         │
                 └────────┬────────┘
                          │
                          ▼
                 ┌─────────────────┐
                 │ Local Rate      │
                 │ Limiter         │
                 └────────┬────────┘
                          │
                          ▼
              ┌──────────────────────┐
              │ LLM Client Boundary  │
              │                      │
              │ timeout              │
              │ error classifier     │
              │ Retry-After parser   │
              │ retry budget         │
              │ backoff + jitter     │
              │ tracing              │
              └──────────┬───────────┘
                         │
                         ▼
                ┌──────────────────┐
                │ Provider SDK     │
                └─────────┬────────┘
                          │
                          ▼
                     LLM API

Mỗi layer có một trách nhiệm khác nhau.

Concurrency control bảo vệ chính application khỏi quá nhiều in-flight work.

Local rate limiter làm smooth traffic trước khi provider phải từ chối nó.

Timeout ngăn một attempt giữ resource vô hạn.

Error classifier quyết định failure có retryable hay không.

Retry-After handling lắng nghe backpressure signal từ provider.

Exponential backoff + jitter xử lý trường hợp không có signal đủ cụ thể.

Retry budget ngăn retry loop kéo dài vô hạn.

Observability cho phép chúng ta biết architecture này thực sự đang hoạt động thế nào.

Nếu thiếu observability, retry rất dễ biến failure thật thành latency bí ẩn.

Một request có thể mất 18 giây và cuối cùng thành công, nhưng metric chỉ ghi:

Text
status = 200
latency = 18s

Trong khi điều chúng ta cần biết là:

Text
attempt_1 = 429
wait      = 1.7s

attempt_2 = 429
wait      = 4.2s

attempt_3 = success

Vì vậy production telemetry nên ít nhất giúp quan sát:

Text
first-attempt success rate
retry count
retry reason
429 rate
5xx rate
retry delay
total retry latency
requests abandoned by deadline
provider/model
token consumption

Chỉ khi đó bạn mới phân biệt được:

Text
"retry đang cứu một lượng nhỏ transient failures"

với:

Text
"retry đang che giấu một capacity problem kéo dài."

14. Những sai lầm phổ biến khi xử lý rate limit

1. Retry mọi exception

Nguyên nhân thường là muốn "an toàn":

Python
except Exception:
    retry()

Nhưng retry permanent errors chỉ tăng latency và load.

Cách tốt hơn là có error taxonomy rõ ràng và allow-list những failure thật sự transient.


2. Dùng exponential backoff nhưng quên jitter

Developer đã nghe về exponential backoff nên triển khai:

Text
1, 2, 4, 8, 16

và nghĩ hệ thống đã hoàn chỉnh.

Nếu hàng trăm worker cùng bắt đầu tại một thời điểm, chúng vẫn có thể retry cùng nhau. AWS simulation cho thấy thêm jitter mới xử lý được phần lớn synchronization problem trong scenario contention này.


3. Thêm retry mà không kiểm tra SDK đã retry hay chưa

Khi cả wrapper và SDK cùng retry, effective attempts có thể lớn hơn nhiều so với developer tưởng.

Anthropic và Gemini đều hiện có retry behavior trong official SDKs, vì vậy đây không phải một edge case mang tính lý thuyết.


4. Chỉ nghĩ bằng request count

Một application có thể có RPM thấp nhưng vẫn chạm token throughput vì prompt quá lớn.

Rate-limit dashboard cần được đọc theo đúng dimension:

Text
request rate
input tokens
output tokens
daily limits
spend/capacity controls

chứ không chỉ một graph requests/second.


5. Dùng retry để thay thế backpressure

Nếu application luôn gửi nhanh hơn provider capacity:

Text
send → 429 → retry

thì 429 đang vô tình trở thành queue của bạn.

Queue nên nằm trong hệ thống bạn kiểm soát, không phải trong error responses của dependency.


6. Không đặt overall deadline

Một request có thể thực hiện nhiều attempt thành công về mặt retry policy nhưng thất bại về mặt UX vì user đã phải đợi quá lâu.

Retry phải nằm trong latency budget của operation.


7. Không kiểm tra side effects

Network timeout không cho bạn biết chắc server đã thực thi operation hay chưa.

Retry chỉ an toàn khi operation vốn idempotent hoặc có cơ chế deduplication/idempotency phù hợp.


15. Từ "handle 429" tới capacity-aware LLM engineering

Quay lại tình huống ban đầu.

200 worker cùng nhận 429.

Phiên bản đầu tiên của hệ thống phản ứng như sau:

Text
429
 ↓
retry immediately
 ↓
429
 ↓
retry immediately

Sau bài viết này, mental model nên khác hoàn toàn.

Text
                    Request
                       │
                       ▼
              Is local capacity OK?
                       │
                       ▼
                    Send
                       │
                       ▼
                    Failure?
                 ┌─────┴─────┐
                 │           │
                no          yes
                 │           │
                 ▼           ▼
              success   classify error
                             │
                    ┌────────┴────────┐
                    │                 │
                permanent         transient
                    │                 │
                    ▼                 ▼
                fail fast      Retry-After?
                                 │       │
                                yes      no
                                 │       │
                                 ▼       ▼
                              obey    backoff
                              hint    + jitter
                                 \       /
                                  \     /
                                   ▼   ▼
                                budget left?
                                 │       │
                                yes      no
                                 │       │
                                 ▼       ▼
                               retry    fail

Đây mới là bản chất của retry engineering.

Rate limit cho biết dependency có một capacity contract.

Backpressure giúp application tôn trọng contract đó trước khi failure xảy ra.

Timeout giới hạn thời gian của từng attempt.

Error classification ngăn retry những request chắc chắn không thể tự phục hồi.

Exponential backoff giảm áp lực khi failure kéo dài.

Jitter ngăn hàng trăm client cùng retry theo một nhịp.

Retry budget đảm bảo resilience không biến thành infinite persistence.

Và khi retry xuất hiện quá thường xuyên, câu trả lời không còn là tăng max_retries từ 5 lên 10.

Khi đó chúng ta phải quay lại architecture và hỏi:

Tại sao application liên tục cố tiêu thụ nhiều capacity hơn mức dependency có thể cung cấp?

Đó là bước chuyển từ một application "có retry" sang một hệ thống thực sự hiểu cách vận hành trong môi trường LLM API.

Những điều quan trọng nhất cần nhớ

  • 429 không phải một root cause duy nhất. Hãy xác định limiter nào đang bị chạm.
  • Chỉ retry transient failures. Permanent failure phải fail fast.
  • Exponential backoff chưa đủ; contention cần jitter.
  • Retry phải có deadline và budget hữu hạn.
  • Kiểm tra retry ở các layer bên dưới để tránh retry amplification.
  • Nếu 429 xuất hiện thường xuyên, hãy giải quyết capacity và traffic architecture thay vì chỉ tinh chỉnh backoff.

Nguồn tham khảo

  • OpenAI Help Center — How can I solve 429: Too Many Requests errors?
  • Anthropic — Rate limits
  • Anthropic — Claude API errors
  • Google AI for Developers — Gemini API Rate Limits
  • Google AI for Developers — Gemini API Troubleshooting: Retry strategy
  • AWS Architecture Blog — Exponential Backoff and Jitter
  • Amazon Builders' Library — Timeouts, Retries, and Backoff with Jitter
  • Amazon Builders' Library — Making Retries Safe with Idempotent APIs
  • Tenacity Documentation — wait_random_exponential
Chia sẻXLinkedIn