10 июл. 2025 г.·7 мин. чтения

Обработка 429: очередь, backoff и понятные состояния для пользователей

Обработка 429 для LLM и API-приложений: ставьте запросы в очередь, корректно используйте backoff и показывайте понятные состояния пользователю, чтобы продукт оставался предсказуемым.

Обработка 429: очередь, backoff и понятные состояния для пользователей

Что на самом деле означает 429 (и почему пользователи это замечают)

429 — это сигнал от провайдера: притормози. Вы отправляете слишком много запросов слишком быстро. Это обычно не значит, что ваш код «сломался». Это значит, что вы достигли лимита, установленного для защиты их систем (а иногда и вашего бюджета).

Пользователи плохо воспринимают обработку 429 даже если приложение в целом работает. Опыт становится неприятным: спиннер, который никогда не заканчивается, сообщение об ошибке после долгого ожидания или ответы, которые иногда приходят, а иногда нет. Худшее — непредсказуемость. Люди не понимают, что будет дальше.

Повторы полезны, но бесконтрольные повторы часто всё ухудшают. Если все упавшие запросы тут же пытаются снова, вы создаёте всплеск трафика в тот самый момент, когда провайдер просит снизить нагрузку. Это может превратить мелкий сбой в длительный простой и быстро истратить квоты.

Цель — не идеальная скорость, а предсказуемое поведение под нагрузкой. Приложение должно вести себя одинаково каждый раз, даже если ответ приходит позже.

Простая мысленная модель — три элемента, работающих вместе:

  • Очередь, чтобы сглаживать всплески вместо массовых параллельных запросов
  • Backoff (часто экспоненциальный с джиттером), чтобы распределять повторы
  • Понятные состояния для пользователя, чтобы он видел, ожидает запрос, повторяется или требуется действие

Пример: чат-приложение получает утренний всплеск. Без контроля половина сообщений падает случайно. С очередью и backoff сообщения могут идти дольше, но пользователи видят «В очереди», затем «Повтор через 4 с». Приложение кажется спокойным и надёжным, а не сломанным.

Откуда обычно берутся лимиты в реальных приложениях

Большинство 429 — не случайные сбои. Это знак, что слишком много запросов попало в лимит одновременно, и ваше приложение не распределило нагрузку.

Частая причина — всплеск: трафик после релиза, пост в соцсетях или большой клиент, пригласивший команду. Пакетные задания тоже это делают, например переиндексация, импорт CSV или бэктрейлы эмбеддингов ночью.

Важно, чей лимит вы пересекаете. Провайдеры часто применяют ограничения по API-ключу, аккаунту или региону. Ваше приложение тоже может создавать лимиты, намеренно или случайно — по пользователю, рабочему пространству или IP (особенно если много пользователей за одним корпоративным NAT).

У LLM-приложений есть несколько паттернов, которые быстрее достигают лимитов, чем вы ожидаете:

  • Стриминг ответов держит соединения открытыми дольше, поэтому конкуренция растёт даже при нормальном QPS.
  • Вызовы инструментов умножают работу: один «ответ» может инициировать дополнительные вызовы для получения данных, выполнения функций и повторного запроса модели.
  • Массовые эмбеддинги — классика: одно действие «импорт документов» может отправить сотни запросов в tight loop.

Скрытые множители — типичный виновник. Один клик может создать много downstream-вызовов:

  • Отправка чата запускает модерацию, основной completion и логирование
  • Одно сообщение может инициировать 3–10 вызовов инструментов до финального ответа
  • Кнопка «Повтор» вызывает запрос немедленно и добавляет давление
  • Загрузка страницы запускает параллельные запросы в нескольких вкладках
  • Фоновый воркер повторяет неудачную задачу по многим аккаунтам

Стейджинг часто выглядит нормально, потому что там один пользователь, чистые данные и нет cron. В продакшне — конкуренция, реальные пики, долгоживущие стриминг-сессии и случайные бури вроде одновременного запуска нескольких воркеров для одного бэфилла.

Выберите понятную политику: повторять, отложить или остановиться

Когда вы получаете 429, худшее решение — «попробовать снова сразу» без правил. Это заставляет провайдера сильнее отталкивать, и приложение кажется хаотичным. Хорошая обработка 429 начинается с простой политики, которой может следовать вся команда.

Разделите запросы на две корзины:

  • Интерактивные действия (пользователь нажал кнопку)
  • Фоновая работа (синхронизации, webhooks, пакетные задания)

Интерактивные действия требуют короткого окна терпения и понятной обратной связи. Фоновые задачи могут ждать дольше, пока они не накопятся бесконечно.

Практическая политика, которую можно принять и потом настроить:

  • Повторяйте только безопасные для повтора запросы (чтение или записи с идемпотентностью).
  • Задайте максимальное окно повторов (например, 10–20 секунд для интерактивных, 5–30 минут для фоновых).
  • Прекращайте и возвращайте ошибку, когда пользователь может исправить поведение (например, многократный клик «Regenerate») или когда не хватает данных.
  • Используйте разные приоритеты: интерактивные запросы идут первыми, фоновые отступают при ужесточении лимитов.
  • Логируйте контекст при каждом 429, чтобы можно было отлаживать паттерны, а не гадать.

Идемпотентность делает повторы безопасными. Если операция создаёт что-то (списание, запись, отправка письма), прикрепляйте идемпотентный ключ, чтобы повтор не дублировал эффект. Если это невозможно, предпочитайте «остановиться и спросить» вместо слепых повторов.

Определите «выполнено» для каждого запроса. Запрос должен либо успешно завершиться, либо вернуть понятную ошибку, либо тайм-аутиться после максимального окна повторов. Висеть вечно — самое раздражающее.

Логируйте достаточно, чтобы ответить на базовые вопросы: какой endpoint был вызван, кто инициировал, интерактивный это был запрос или фоновый, когда начался и сколько ждал.

Пошагово: добавьте очередь запросов, которая сглаживает всплески

Самый быстрый способ улучшить обработку 429 — перестать посылать запросы сразу при действии UI. Вместо этого помещайте каждый вызов в очередь. Это превращает всплеск из 50 кликов в контролируемый поток, который провайдер может принять.

1) Начните с маленькой очереди между UI и провайдером

Сделайте первую версию простой. Когда приложение хочет вызвать LLM или API, создавайте джоб с полезной нагрузкой и метками (кто спросил, для какого экрана, когда создано). UI отправляет джобы в очередь, а не напрямую провайдеру.

Практическая форма джоба включает: endpoint/model, хэш промпта или параметров, ID пользователя, приоритет и счётчик повторов.

2) Добавьте воркер с ограничением конкурентности

Воркер забирает джобы из очереди и выполняет их. Ключевой параметр — concurrency: сколько задач может выполняться одновременно. Начинайте с малого (1–3), увеличивайте только при стабильных результатах.

Простой подход, который работает в большинстве приложений:

  • Обрабатывайте не более N джобов параллельно (лимит конкурентности)
  • Когда джоб завершился, сразу запускайте следующий
  • Если джоб получил 429, ставьте его обратно в очередь с задержкой (backoff описан ниже)
  • Отслеживайте активные джобы, чтобы никогда не превышать N

3) Приоритеты: сохраняйте отзывчивость приложения

Не все запросы равны. Кнопка, которую только что нажал пользователь, должна иметь приоритет над фоновыми задачами вроде «суммаризации всего» или «генерации еженедельного отчёта». Начните с двух приоритетов (высокий и низкий). Если очередь поддерживает, заведите для высокого отдельную полосу.

4) Дедуплицируйте повторяющиеся запросы

Пользователи двойные кликают. Фронтенды ререндерят. Если вы получаете идентичные промпты в коротком окне (2–10 секунд), объединяйте их. Возвращайте один и тот же результат в полёте всем вызывающим. Это само по себе резко снизит объём запросов.

5) Храните состояние очереди, чтобы перезапуски не теряли работу

Если сервер перезапустится, не хочется, чтобы ожидающие джобы исчезли или повторялись непредсказуемо. Сохраняйте очередь и статусы в надёжном хранилище (БД или очередной системе). Здесь многие прототипы и падают: запросы fire-and-forget, и пользователи видят пропавшие ответы, дубликаты или вечные спиннеры при срабатывании лимитов.

Пошагово: backoff, который снижает нагрузку, а не усиливает её

Аудит вашей политики повторных попыток
Проверьте идемпотентность, временные бюджеты и обработку `Retry-After` во всём приложении.

Хорошая обработка 429 — в основном про то, чтобы делать меньше, а не упорно пытаться снова. Если вы ретраите слишком быстро, вы превращаете небольшую заминку в всплеск и держите пользователей в цикле.

Начните с экспоненциального backoff: после первого 429 подождите немного, после следующего — дольше. Простое правило — удваивать задержку. Это даёт провайдеру время восстановиться и не даёт вам молотить по API.

Добавьте джиттер (небольшую случайную величину) к каждой паузе. Без джиттера тысячи клиентов, попавших в лимит одновременно, снова ретраят в один момент и снова попадают в лимит. Джиттер распределяет попытки, давая приложению шанс пройти.

Если провайдер даёт подсказку вроде Retry-After, считайте её источником истины. Используйте её как базовую задержку и добавляйте небольшую случайность вокруг неё.

Задайте явные ограничения, чтобы повторы не длились вечно:

  • Максимум попыток (например, 3–6)
  • Максимальная задержка (например, лимит 30–60 секунд)
  • Общий временной бюджет (например, остановиться после 2 минут)
  • Резервный путь (сохранить задачу, попросить пользователя попробовать позже)

Переставайте ретраить на ошибках, которые не исправятся сами по себе. 400 будет падать всегда. 401/403 — проблемы с аутентификацией. Повторяйте только там, где вероятность успеха есть (429, многие 5xx и некоторые сетевые таймауты).

Пример: чат-функция получает 429 при релизе. Попытка 1 ждёт 1–2 секунды, попытка 2 — 2–4 секунды, попытка 3 — 4–8 секунд, затем вы прекращаете и показываете понятное сообщение, что система занята и сообщение отправится, когда появится возможность.

Состояния для пользователя, которые делают приложение предсказуемым

Когда вы получаете лимит, худший результат — не задержка, а путаница. Если UI выглядит замороженным или постоянно переключается между ошибкой и спиннером, пользователи будут нажимать снова, обновлять страницу или открывать вторую вкладку. Это порождает дополнительные запросы и удлиняет проблему.

Хорошая обработка 429 начинается с простого названия того, что происходит, и одного понятного следующего шага.

Используйте небольшой набор чётких состояний

Сделайте состояния едиными по всему приложению. Большинству команд хватает трёх:

  • В очереди: «Мы в очереди, чтобы отправить ваш запрос.»
  • Повторяется: «Провайдер загружен. Повтор через 10–20 секунд.»
  • Попробовать снова: «Мы пока не смогли отправить. Попробуйте ещё раз.»

По возможности показывайте ориентировочное время ожидания, даже если это только диапазон. Можно основывать на позиции в очереди (например, 3 запроса впереди) или на таймере backoff. Небольшой обратный отсчёт помогает людям не нажимать лишний раз.

Предложите безопасную кнопку Отмена для длительного ожидания. Объясните одно предложение: «Отмена прекращает повторы. Ваш черновик остаётся, и ничего не отправляется.» Если при отмене теряется работа — скажите об этом и предложите «Сохранить черновик».

Избегайте сырых ошибок типа «429» или «Too Many Requests». Переведите это на язык пользователя: «Провайдер ИИ просит нас притормозить.» Затем предложите одно действие: ждать (по умолчанию) или попробовать снова.

Для ожиданий длиннее нескольких секунд покажите лёгкое уведомление, когда результат готов, например in-app баннер «Ваш ответ готов» или изменение статуса сообщения. Это особенно важно в чате.

Защитите остальную часть приложения, пока провайдер вас тормозит

Когда провайдер начинает отдавать 429, главный риск — не сам упавший вызов, а эффект домино: потоки блокируются, очереди растут без предела, и несвязанные части приложения становятся медленными. Хорошая обработка 429 — в основном про локализацию проблемы.

Начните с таймаутов везде, где вызов провайдера может зависнуть. Повтор, который ждёт вечно, — это не терпение, а блокировка. Установите понятный максимум времени на попытку и общий максимум по всем попыткам, чтобы один запрос не держал систему заложником.

Цепной автомат (circuit breaker) полезен при всплесках 429. Вместо того чтобы каждый запрос продолжал бить по провайдеру, приостановите вызовы на короткое окно, а затем проверьте состояние мелким тестовым запросом. Это делает замедление предсказуемым и останавливает дальнейшее повышение нагрузки.

Отделяйте отзывчивость UI от скорости провайдера. UI должен реагировать мгновенно: принять действие пользователя, показать понятный статус и обработать работу в фоне. Если вы привязываете нажатие кнопки к времени ответа провайдера, всё приложение будет казаться сломанным, даже если только одна зависимость медленная.

Бюджеты по пользователю предотвращают, чтобы один тяжёлый пользователь (или баг) не съедал всех ресурсов. Сделайте просто: сколько задач в очереди и сколько времени повтора один пользователь может потреблять, прежде чем вы начнёте задерживать или отклонять запросы.

Полезные метрики для наблюдения:

  • 429 по времени (всплески vs стабильный уровень)
  • Глубина очереди (сколько задач ждёт)
  • Среднее время ожидания перед началом выполнения
  • Частота таймаутов (слишком жёсткие vs слишком мягкие)
  • Время, когда circuit breaker открыт (как часто вы приостанавливаете вызовы)

Распространённые ошибки, которые усугубляют проблемы с 429

Сделать унаследованный AI-код поддерживаемым
Мы делаем унаследованные AI-сгенерированные приложения поддерживаемыми, чтобы исправления сохранялись после релиза.

Большая часть боли от 429 — из нескольких предсказуемых ошибок. Избегайте их, и обработка 429 станет скучной (в хорошем смысле).

Повторы, создающие шторм повторов

Мгновенные повторы или повторы с фиксированным интервалом могут превратить небольшую заминку в отключение. Если 200 пользователей нажмут «Попробовать снова» и каждый клиент ретраит в одну секунду, вы получите синхронизированную волну трафика, которая вновь вызовет 429.

Используйте растущую задержку со временем и джиттер, чтобы попытки распределялись.

Бесконечные повторы и скрытые расходы

Без лимита по попыткам и общему времени повторы могут разрастись в бесконечный цикл. Это съест токены или кредиты API, загрузит воркеры и создаст непонятные «загрузка вечность» экраны.

Ограничьте повторы и остановитесь с понятным сообщением, когда достигнут лимит.

Один глобальный лимит без приоритетов

Если вы обращаетесь со всеми вызовами одинаково, фоновые задачи (эмбеддинги) могут съесть критические потоки вроде логина, чекаута или «Отправить сообщение». Используйте отдельные очереди или приоритеты, чтобы ключевые действия всегда были в приоритете.

Игнорирование частичных успехов

Частый сценарий: один вызов успешен (создали сообщение), а следующий получает 429 (не удалось получить обновлённую историю). Если считать всю операцию проваленной, вы рискуете дубликатами, отсутствием обновлений UI или нарушением состояния.

Обращайтесь с каждым вызовом как с отдельным шагом. Сохраняйте то, что успешно, и повторяйте только то, что упало.

Общие ошибки, которые побуждают пользователей нажимать чаще

«Что-то пошло не так» заставляет людей долбить кнопку, что создаёт ещё больше нагрузки. Показывайте конкретное состояние: «Мы ждём провайдера. Повтор через 8 секунд.»

Быстрый чек-лист перед релизом

Перед релизом воспринимайте обработку 429 как фичу продукта, а не просто цикл повторов. Хорошая настройка контролирует затраты, избегает неожиданных простоев и делает приложение спокойным, даже когда провайдер просит «притормозить».

Практический предрелизный чек:

  • Конкурентность ограничена в двух местах: для фичи (например чат) и для пользователя (чтобы один активный не отъел всех).
  • Повторы имеют явные лимиты: максимум попыток и общий максимум времени на повторы (чтобы запросы не зависали).
  • Backoff снижает нагрузку: добавлен джиттер, учитываются подсказки сервера вроде Retry-After, и избегаются синхронные повторы среди многих пользователей.
  • UI остаётся предсказуемым: пользователи видят, что происходит («Ожидает повтора»), сколько примерно это займёт и безопасные действия (Отмена, Попробовать снова или Продолжить без результата).
  • Логи достаточны для быстрой отладки: указывайте имя провайдера, endpoint/model, ID запроса, ID пользователя (или анонимный токен), номер попытки, выбранную задержку и точную полезную нагрузку ошибки.

Сделайте быстрый стресс-тест перед релизом. Откройте приложение в двух браузерах, быстро инициируйте 10–20 действий и убедитесь в упорядоченной очереди, растущих задержках и корректной остановке при достижении лимитов.

Пример: чат, пострадавший от внезапного всплеска

Стабилизировать за 48–72 часа
Большинство проектов стабилизируются за 48–72 часа после локализации узких мест.

Представьте командный демонстрационный показ: кто‑то делится ссылкой, и за минуту 50 человек открывают чат и отправляют один и тот же промпт. Ваше приложение шлёт 50 почти идентичных вызовов к LLM. Через несколько секунд начинаются 429.

Без контроля всё выглядит случайным. Некоторые запросы таймаутятся, некоторые сразу ретраят и снова блокируются, пользователи начинают нажимать Send по несколько раз. В итоге дублируются запросы, растут расходы и ответы не совпадают (у одного — ответ, у другого — ошибка, у третьего — два ответа).

Очередь меняет форму трафика. Вместо всплеска вы выстраиваете запросы и выпускате их равномерно. Добавьте приоритет, чтобы приложение оставалось отзывчивым: критичные UI-действия (отмена сообщения, загрузка истории чата, получение профиля) идут первыми, а генерация ждёт своей очереди.

Backoff не даёт вам усугублять ситуацию, когда провайдер уже «жмёт тормоз». Получив 429, вы ретраите через растущий интервал (экспоненциальный backoff) плюс небольшую случайность (джиттер). Это препятствует одновременному ретраю всех 50 клиентов и новому коллапсу.

То, что видит пользователь, так же важно, как и код. В период всплеска предсказуемый UI может показывать:

  • «В очереди» с оценкой ожидания вроде 20–40 секунд
  • Чёткую кнопку Отмена, которая действительно останавливает задачу
  • Опцию Повтор только после фактического провала, а не во время обычного ожидания
  • Одно сообщение на промпт (без дублей), меняющее статус с В очереди на Отправляется и Готово

Следующие шаги: реализуйте базу, затем сделайте надёжным

Начните со страницы, где описаны ваши правила: когда вы ретраите, сколько ждёте, когда останавливаетесь и что видит пользователь на каждом этапе. Это предотвращает типичную путаницу, где бэкенд ретраит вечно, а UI кажется зависшим.

Если у вас только две инженерные задачи на неделю, сделайте их там, где боль сильней: в общем клиенте, который общается с провайдером. Централизованная обработка 429 приносит пользу всем фичам и не даёт иметь пять разных поведений повторов по всему приложению.

Практическая последовательность, которая обычно работает:

  • Определите политику повторов (максимум попыток, максимум ожидания и какие ошибки повторяемы)
  • Добавьте очередь запросов, чтобы сгладить всплески от кликов и фоновых задач
  • Внедрите экспоненциальный backoff с джиттером, чтобы повторы снижали нагрузку, а не наращивали её
  • Добавьте понятные состояния для пользователя: ожидает, повторяется и безопасный сценарий при сдаче с попытками
  • Логируйте каждый 429 с выбранной задержкой, чтобы видеть паттерны позже

Затем запустите простой нагрузочный тест. Сымитируйте 20–50 быстрых действий (send message, regenerate, refresh data) и намеренно верните несколько 429. Цель не скорость, а то, чтобы UI оставался понятным: пользователи должны видеть, что приложение ждёт, а не сломано, и знать, что будет дальше.

Если вы унаследовали AI-сгенерированный код, будьте осторожны с накладыванием патчей. Именно так появляются штормы повторов и призрачные запросы, которые продолжают выполняться после того, как пользователь ушёл со страницы.

Если нужен второй взгляд, FixMyMess (fixmymess.ai) фокусируется на диагностике и исправлении AI-сгенерированных приложений, которые разваливаются под реальным трафиком: runaway retries, отсутствующие лимиты и беспорядочный фэн-аут запросов. Быстрый аудит поможет быстро вернуть предсказуемое поведение.

Часто задаваемые вопросы

Что на самом деле означает ошибка 429?

429 означает, что провайдер ограничивает вас по скорости, потому что за короткое время пришло слишком много запросов. Это не обязательно ошибка в коде — чаще виноват паттерн трафика (всплески, высокая конкуренция или скрытые дополнительные вызовы), который превышает квоту или пропускную способность.

Что должно делать моё приложение при получении 429?

По умолчанию — ждать и повторять с задержкой, а не пытаться моментально снова. Установите короткий временной бюджет для действий, инициированных пользователем, и более длительный для фоновых задач. Остановите повторы, когда исчерпан бюджет, чтобы приложение не висло бесконечно.

Почему рекомендуют экспоненциальный backoff и джиттер?

Экспоненциальный backoff увеличивает паузу между попытками, когда провайдер перегружен. Джиттер добавляет небольшую случайность, чтобы множество клиентов не ретраяли одновременно и не вызвали новую волну 429.

Стоит ли следовать заголовку Retry-After?

Обращайте внимание на Retry-After как на главный источник истины по тому, как долго ждать. Если добавляете джиттер, делайте его небольшим вокруг этого значения, чтобы уважать рекомендации провайдера и избежать синхронных повторов.

Действительно ли нужна очередь запросов или достаточно просто повторять?

Очередь сглаживает всплески, превращая «50 запросов сейчас» в контролируемый поток, который провайдер может принять. Она также даёт место для приоритетов, задержек и устойчивого хранения, чтобы запросы не пропадали и не дублировались при перезапусках.

Как подобрать безопасный лимит конкурентности?

Начните с низкой конкруентности (обычно 1–3 на воркер или на фичу) и увеличивайте только после стабильных результатов в условиях, близких к боевым. Правильное значение — максимальное, которое не вызывает частых 429 при обычных пиках.

Когда повторы опасны и как их обезопасить?

Повторяйте только те запросы, которые безопасно выполнять снова: чтение или записи с идемпотентным ключом. Если повтор может создать дубликаты (списание, письмо, запись), сначала сделайте операцию идемпотентной или завершите с ошибкой и попросите подтверждение пользователя.

Стоит ли дедуплицировать одинаковые промпты или API-вызовы?

Да. Если в коротком окне приходят одинаковые запросы, верните тот же результат для всех запросов «в полёте», вместо того чтобы запускать новые вызовы. Это уменьшает трафик при двойных кликах и ререндерингах фронтенда.

Что должны видеть пользователи, пока запрос ждёт или повторяется?

Показывайте простые, согласованные состояния: «В очереди», «Повтор через X секунд» и понятное «Попробовать снова», когда вы прекратили попытки. Главное — интерфейс сразу признаёт действие и объясняет, что происходит, чтобы пользователи не нажимали лишний раз.

Что нужно логировать и измерять, чтобы исправить 429 навсегда?

Логируйте контекст, который поможет выявить закономерности: endpoint/model, пользователь или workspace, интерактивный запрос или фоновая задача, номер попытки, выбранная задержка и общее время. Если унаследованный AI-код содержит runaway retries, множитель вызовов инструментов или отсутствующие лимиты, FixMyMess может провести аудит и быстро исправить эти сценарии, чтобы поведение стало предсказуемым.