Rate-limit 429 от провайдера
429 Too Many Requests — это не авария провайдера, а сигнал, что вы упёрлись в свой лимит (RPM/TPM). Один 429 — норма, SDK его сам ретраит. А вот когда их десятки в минуту — половина запросов уходит в бэкофф, latency у пользователей растёт, и пора либо троттлить свою нагрузку, либо просить у провайдера более высокий tier.
Полный аутдаж ловится мониторингом доступности; здесь — именно про 429 в скользящем окне.
Считаем 429 вокруг вызова клиента
Заголовок раздела «Считаем 429 вокруг вызова клиента»Оборачиваем вызов SDK: ловим rate-limit ошибку, кладём timestamp в скользящее окно и алёртим при превышении порога за минуту.
import os, time, collections, requestsimport 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: превышено число ретраев")
# использование — как обычный вызов, но с учётом 429resp = chat(model="gpt-4o-mini", messages=[{"role": "user", "content": "ping"}])Ключевые детали: deque с выбросом старых меток даёт настоящее скользящее
окно; COOLDOWN не даёт зафлудить телефон, пока лимит держится; Retry-After
из заголовка уважается, а не игнорируется.
Отделяйте 429 от 529 / 503
Заголовок раздела «Отделяйте 429 от 529 / 503»У Anthropic перегрузка приходит как 529 Overloaded — это про сторону
провайдера, а не про ваш лимит, и лечится иначе (подождать, а не троттлить
себя). Считайте их отдельным счётчиком и шлите отдельный алёрт, иначе
перепутаете «я слишком быстрый» с «у них жарко».
Многопроцессный вариант — счётчик в Redis
Заголовок раздела «Многопроцессный вариант — счётчик в Redis»Если запросы идут из нескольких воркеров, deque в памяти одного процесса
не увидит общей картины. Держите скользящее окно в Redis:
import redis, timer = 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 в один клик.