Перейти к содержимому

Rate-limit 429 от провайдера

429 Too Many Requests — это не авария провайдера, а сигнал, что вы упёрлись в свой лимит (RPM/TPM). Один 429 — норма, SDK его сам ретраит. А вот когда их десятки в минуту — половина запросов уходит в бэкофф, latency у пользователей растёт, и пора либо троттлить свою нагрузку, либо просить у провайдера более высокий tier.

Полный аутдаж ловится мониторингом доступности; здесь — именно про 429 в скользящем окне.

Оборачиваем вызов SDK: ловим rate-limit ошибку, кладём timestamp в скользящее окно и алёртим при превышении порога за минуту.

import os, time, collections, requests
import openai # или anthropic — принцип тот же
NOTIFLY_URL = os.environ["NOTIFLY_URL"]
NOTIFLY_TOKEN = os.environ["NOTIFLY_TOKEN"]
WINDOW_SEC = 60 # ширина окна
THRESHOLD = 10 # столько 429 за окно = алёрт
COOLDOWN = 300 # не дублировать алёрт чаще, чем раз в 5 минут
client = openai.OpenAI()
_hits = collections.deque() # timestamps последних 429
_last_alert = 0.0
def notify(title, msg, prio):
requests.post(f"{NOTIFLY_URL}/message",
params={"token": NOTIFLY_TOKEN},
json={"title": title, "message": msg, "priority": prio},
timeout=5)
def _record_429():
global _last_alert
now = time.time()
_hits.append(now)
while _hits and _hits[0] < now - WINDOW_SEC: # выбрасываем старьё
_hits.popleft()
if len(_hits) >= THRESHOLD and now - _last_alert > COOLDOWN:
_last_alert = now
notify(
f"🚦 Rate-limit: {len(_hits)} × 429 за {WINDOW_SEC}с",
"Провайдер режет по лимиту. Сбавьте темп или поднимите tier.",
priority=7,
)
def chat(**kwargs):
"""Обёртка вокруг вызова: считает 429, ретраит с бэкоффом."""
for attempt in range(5):
try:
return client.chat.completions.create(**kwargs)
except openai.RateLimitError as e:
_record_429()
# уважаем Retry-After, если провайдер его прислал
wait = float(getattr(e, "response", None)
and e.response.headers.get("retry-after", 0)) or 2 ** attempt
time.sleep(wait)
raise RuntimeError("rate-limited: превышено число ретраев")
# использование — как обычный вызов, но с учётом 429
resp = chat(model="gpt-4o-mini", messages=[{"role": "user", "content": "ping"}])

Ключевые детали: deque с выбросом старых меток даёт настоящее скользящее окно; COOLDOWN не даёт зафлудить телефон, пока лимит держится; Retry-After из заголовка уважается, а не игнорируется.

У Anthropic перегрузка приходит как 529 Overloaded — это про сторону провайдера, а не про ваш лимит, и лечится иначе (подождать, а не троттлить себя). Считайте их отдельным счётчиком и шлите отдельный алёрт, иначе перепутаете «я слишком быстрый» с «у них жарко».

Если запросы идут из нескольких воркеров, deque в памяти одного процесса не увидит общей картины. Держите скользящее окно в Redis:

import redis, time
r = redis.Redis()
def record_429_shared():
now = time.time()
key = "llm:429"
r.zadd(key, {f"{now}": now})
r.zremrangebyscore(key, 0, now - WINDOW_SEC) # чистим старьё
return r.zcard(key)

Порог проверяется по возвращённому числу — так все воркеры вместе решают, когда пора слать push.

  • какой провайдер/модель режет (у каждого свой лимит);
  • число 429 за окно и текущий тренд;
  • значение Retry-After, если провайдер его прислал;
  • ссылку на страницу лимитов в dashboard, чтобы поднять tier в один клик.