Массовая обработка запросов без упора в лимит скорости
Один ключ с лимитом на батч-задаче — гарантированный 429 на середине прогона. Разбираем, как строить массовую обработку так, чтобы это не случилось.
4 мин · · Обновлено · разработка
Батч-задача на тысячи запросов ведёт себя иначе, чем интерактивный чат: ошибки накапливаются, ретраи множатся, а лимит на ключе, который был незаметен в разработке, за час прогона превращается в стену. Ниже — конкретные приёмы, которые снимают эту стену, не отключая лимиты вообще (это не всегда возможно и не всегда разумно), а работая с ними.
Понять, какой именно 429 прилетел
Своего лимита скорости у шлюза нет. 429 от самого Zukko означает исчерпанный лимит расхода конкретного ключа, и код в ответе прямо это говорит — key_daily_limit_reached или key_monthly_limit_reached. Любой другой 429 пришёл от провайдера модели. Это принципиально разные ситуации. 429 провайдера — сигнал притормозить и повторить через экспоненциальный backoff, он снимется сам через какое-то время. Лимит ключа снимется только с началом нового дня (сброс в 00:00 UTC) или месяца либо после ручного изменения лимита в кабинете — долбить его ретраями бессмысленно, воркер должен переключиться на другой ключ или остановиться.
if resp.status_code == 429:
code = resp.json().get("error", {}).get("code")
if code in ("key_daily_limit_reached", "key_monthly_limit_reached"):
switch_to_next_key()
else:
time.sleep(backoff_seconds)Разделить нагрузку на несколько ключей
На странице ключа в кабинете для каждого ключа задаётся свой дневной/месячный лимит и список разрешённых моделей. Для батч-обработки логично завести пул из нескольких ключей и распределять запросы между ними — например, по хэшу id задачи или по подзадачам. Пропускную способность это не увеличивает (ограничения скорости на шлюзе нет, а баланс у всех ключей один — аккаунта), зато каждая часть прогона получает свой бюджет: зациклившаяся подзадача исчерпает лимит своего ключа, а не весь бюджет батча.
Идемпотентные ретраи вместо голых
Сетевой обрыв на середине батч-прогона — это не про лимит токенов, это таймаут клиента или сети, и стоит перепроверить max_tokens и таймауты HTTP-клиента, прежде чем что-то менять в логике ретраев. Сам ретрай нужно делать с заголовком Idempotency-Key, сохраняя одно и то же значение для одного и того же логического запроса:
import uuid
idempotency_key = str(uuid.uuid4())
for attempt in range(3):
resp = requests.post(
"https://api.zukko.pro/v1/chat/completions",
headers={
"Authorization": f"Bearer {api_key}",
"Idempotency-Key": idempotency_key,
},
json=payload,
timeout=30,
)
if resp.status_code < 500 and resp.status_code != 429:
break
time.sleep(2 ** attempt)Если исходный запрос на самом деле дошёл до модели и обработался, повтор с тем же Idempotency-Key в течение 24 часов вернёт тот же ответ без повторного списания — это критично именно в батче, где сумма случайных двойных списаний на тысячах запросов заметна на счету.
Кэш для повторяющихся входов
Если в батче много полностью одинаковых запросов — например, классификация повторяющихся значений, — стоит включить кэш заголовком X-Zukko-Cache: true: это основной способ, он работает для любой модели. temperature: 0 тоже включает кэш, но только для моделей, которые принимают это значение; новые модели Claude (claude-sonnet-5, claude-opus-5-5) могут отклонить temperature, отличную от 1, с ошибкой 400, поэтому в запросах к ним temperature не передают. Одинаковые запросы кэшируются на срок до 7 суток (для чата), повтор возвращается мгновенно и бесплатно, а в ответе виден заголовок X-Zukko-Cache: hit. Важно: кэш срабатывает только при полном совпадении тела запроса — «почти одинаковые» запросы и запросы с общей фиксированной частью промпта (шаблон + разные данные) из кэша не отдаются. Попадания в кэш не пишутся в /v1/usage. Для батч-джоба это не оптимизация ради оптимизации: если во входных данных 30% точных дублей, кэш реально снимает почти треть стоимости прогона.
resp = requests.post(url, headers={**headers, "X-Zukko-Cache": "true"}, json=payload)
if resp.headers.get("X-Zukko-Cache") == "hit":
cache_hits += 1Следить за балансом по ходу прогона, а не после
Каждый обычный (нестриминговый) ответ несёт X-Zukko-Cost-Micros (стоимость этого запроса в микродолларах, 1,000,000 = $1) и X-Fetch-Balance-Cents (остаток после списания — несмотря на название, тоже в микродолларах; точный баланс в центах — GET /v1/balance). У потоковых ответов (stream: true) этих заголовков нет, поэтому батч лучше гонять без стриминга. На батче из тысяч запросов это позволяет посчитать среднюю стоимость запроса по первым сотне и экстраполировать на весь объём ещё до того, как прогон дойдёт до середины — и остановиться заранее, если баланса объективно не хватит, вместо того чтобы поймать 402 на 6000-м запросе из 10000 и разбираться, какая часть данных обработана.
cost_micros = int(resp.headers.get("X-Zukko-Cost-Micros", 0))
running_total += cost_microsПроверить каталог перед большим прогоном, а не хардкодить модель
GET /v1/models отдаёт актуальный список моделей (только id, object, created, owned_by), а GET /v1/models/info — их цены и лимиты контекста. Перед запуском батча на несколько часов имеет смысл дёрнуть эти эндпоинты и свериться с тем, что зашито в конфиге — если модель успели выключить, лучше узнать об этом заранее, чем поймать 503 model_disabled на середине прогона и потерять частично обработанный батч.
curl https://api.zukko.pro/v1/models/info503 и model_disabled — это не повод падать целиком
503 provider_unavailable или model_disabled означает временное отключение модели в каталоге, а не проблему с запросом. На батче разумно ловить именно эти коды отдельно от прочих 5xx и откладывать соответствующие элементы очереди в отдельный список для повторной обработки позже, а не ронять весь джоб или тратить те же самые ретраи, что и на обычный сетевой сбой.
Собрать всё вместе
Рабочий батч-раннер в итоге выглядит так: пул ключей с настроенными лимитами, Idempotency-Key на каждый логический запрос, заголовок X-Zukko-Cache: true там, где вход предсказуемо повторяется, чтение X-Zukko-Cost-Micros для прогнозирования остатка бюджета и отдельная обработка 429 с кодом ключа, 503/model_disabled и обычных сетевых таймаутов. Ни один из этих пунктов не требует ничего сверх стандартных HTTP-заголовков и кодов ответа — вся логика собирается поверх обычного requests или httpx, без специального SDK.
zukko
Платите в 25×
меньше
за Claude Code & ChatGPT
Один ключ fetch_* и base URL api.zukko.pro — в Cursor, Claude Code, Codex и любом SDK.
Похожие статьи
- Промпт-инъекция: что это и как защитить бота и агентаОбъясняем, как работает промпт-инъекция через пользовательский ввод и чужие документы, и какие меры снижают ущерб: изоляция данных, права инструментов, лимиты ключа.
- Свой прокси к нейросети на Node.js: не показывать API-ключ во фронтендеНебольшой сервер на Express между вашим фронтендом и API нейросети: ключ остаётся на сервере, а модели, длина запроса и частота ограничены.
- DeepSeek API на Python: запросы, стриминг, JSON и обработка ошибокГотовый код для DeepSeek V4.1 Flash и V4 Pro на OpenAI SDK: запрос, стриминг, разбор JSON и аккуратная обработка ошибок.
- Промпт-инжиниринг: примеры и 12 готовых шаблонов для работыДвенадцать шаблонов промптов для рабочих задач: что в них написано, почему именно так и сколько стоит один вызов по API.