Добавить в корзинуПозвонить
Найти в Дзене
Vibecode Wiki

Что делать, если API иногда не отвечает: очереди, повторные попытки и circuit breaker простыми словами

Вы написали код, который обращается к внешнему сервису — к API погоды, к платёжной системе, к сервису отправки email. Всё работает на вашем компьютере. Вы запускаете в продакшене — и раз в несколько часов что-то идёт не так. API отвечает с задержкой, возвращает ошибку, или вообще не отвечает. Пользователь видит белый экран или ошибку 500. Это не баг в вашем коде. Это нормальная жизнь: любой внешний сервис иногда недоступен. Вопрос в том, как ваше приложение ведёт себя в этот момент. В этой статье — три инструмента, которые решают проблему: повторные попытки, очереди и circuit breaker. Объясняем каждый простыми словами, с аналогиями, и показываем как это выглядит в коде. Прежде чем лечить — стоит понять причины. API может не ответить по-разному, и это важно, потому что разные причины требуют разных решений. Временная перегрузка. Сервер справляется с нагрузкой, но прямо сейчас получил слишком много запросов. Через секунду-две — снова работает. Решение: подождать и попробовать ещё раз. Пр
Оглавление

Вы написали код, который обращается к внешнему сервису — к API погоды, к платёжной системе, к сервису отправки email. Всё работает на вашем компьютере. Вы запускаете в продакшене — и раз в несколько часов что-то идёт не так. API отвечает с задержкой, возвращает ошибку, или вообще не отвечает. Пользователь видит белый экран или ошибку 500.

Это не баг в вашем коде. Это нормальная жизнь: любой внешний сервис иногда недоступен. Вопрос в том, как ваше приложение ведёт себя в этот момент.

В этой статье — три инструмента, которые решают проблему: повторные попытки, очереди и circuit breaker. Объясняем каждый простыми словами, с аналогиями, и показываем как это выглядит в коде.

Почему API иногда не отвечает

Прежде чем лечить — стоит понять причины. API может не ответить по-разному, и это важно, потому что разные причины требуют разных решений.

Временная перегрузка. Сервер справляется с нагрузкой, но прямо сейчас получил слишком много запросов. Через секунду-две — снова работает. Решение: подождать и попробовать ещё раз.

Превышение лимита запросов (rate limit). Вы отправили слишком много запросов за короткое время. API говорит «стоп, подожди минуту». Решение: соблюдать очередь.

Сеть. Пакет потерялся по пути. Ваш сервер и сервер API оба живые, но конкретный запрос не дошёл. Решение: повторить.

API реально упал. Что-то сломалось на стороне провайдера, и он будет лежать час или два. Решение: прекратить попытки на время и продолжить позже.

Медленный ответ. API живой, но отвечает за 30 секунд вместо обычных 2. Решение: не ждать вечно, поставить таймаут.

Каждый случай требует своей стратегии. Сейчас разберём их по порядку.

Повторные попытки (retry): попробуй ещё раз, но с умом

Что это и зачем

Самая простая идея: если запрос не получился — попробуй ещё раз. Как когда звонишь кому-то и слышишь «абонент недоступен» — кладёшь трубку и перезваниваешь через минуту.

Наивная реализация выглядит так:

// Плохой retry: три попытки подряд, без паузы

async function fetchData() {

for (let i = 0; i < 3; i++) {

try {

return await api.get('/data');

} catch (error) {

if (i === 2) throw error; // последняя попытка — пробрасываем ошибку

}

}

}

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

Пауза между попытками: экспоненциальная задержка

Правильный retry — это retry с нарастающей паузой. Не получилось — подождал секунду. Снова не получилось — подождал две секунды. Ещё раз — четыре секунды. И так далее.

Это называется экспоненциальная задержка (exponential backoff). Слово звучит сложно, но идея простая: каждая следующая пауза вдвое длиннее предыдущей.

Попытка 1: запрос → ошибка → пауза 1 секунда

Попытка 2: запрос → ошибка → пауза 2 секунды

Попытка 3: запрос → ошибка → пауза 4 секунды

Попытка 4: запрос → успех ✓

Зачем нарастающая, а не фиксированная пауза? Если сервер восстанавливается, он сначала едва справляется с нагрузкой. Фиксированные паузы создают одинаковую волну запросов снова и снова. Нарастающие паузы дают серверу всё больше времени на восстановление.

Jitter: добавляем случайность

Представьте, что у вас 100 пользователей одновременно получили ошибку. Все 100 ждут ровно 1 секунду и одновременно повторяют запрос. Сервер снова падает под нагрузкой. Все ждут 2 секунды — и снова синхронный удар.

Jitter (от английского «дрожание») — это небольшой случайный сдвиг в задержке. Вместо ровно 1 секунды — от 0.8 до 1.2 секунды. Звучит как мелочь, но 100 пользователей теперь распределяются во времени, а не бьют синхронно.

// Retry с экспоненциальной задержкой и jitter

async function withRetry(fn, maxAttempts = 3) {

for (let attempt = 1; attempt <= maxAttempts; attempt++) {

try {

return await fn();

} catch (error) {

// Если это последняя попытка — пробрасываем ошибку

if (attempt === maxAttempts) throw error;

// Базовая задержка: 1s, 2s, 4s...

const baseDelay = 1000 * Math.pow(2, attempt - 1);

// Jitter: ±25% от базовой задержки

const jitter = baseDelay * 0.25 * (Math.random() * 2 - 1);

const delay = Math.round(baseDelay + jitter);

console.log(`Попытка ${attempt} не удалась. Повтор через ${delay}мс...`);

await new Promise(resolve => setTimeout(resolve, delay));

}

}

}

// Использование — ваш код не меняется, просто оборачиваете вызов

const data = await withRetry(() => api.get('/weather'));

Важно: не всё нужно ретраить

Повторная попытка имеет смысл только для временных проблем. Если API вернул ошибку «неверный API-ключ» — сколько бы раз вы ни повторяли, ответ не изменится. Ретраить имеет смысл только:

  • сетевые ошибки (соединение разорвалось)
  • статус 429 (слишком много запросов — подождать)
  • статус 500, 502, 503, 504 (временные проблемы сервера)

Не нужно ретраить:

  • 400 (неправильный запрос — исправьте запрос)
  • 401 (неверный ключ — проверьте ключ)
  • 403 (нет прав — обратитесь к провайдеру)
  • 404 (ресурс не существует — не существует)

Очереди: делай работу по очереди, не теряй задачи

Проблема без очереди

Представьте, что ваш сайт при регистрации пользователя должен отправить приветственное письмо через email-сервис. Вы пишете:

app.post('/register', async (req, res) => {

await createUser(req.body);

await sendWelcomeEmail(req.body.email); // вот здесь

res.json({ ok: true });

});

Что происходит, если email-сервис в момент регистрации недоступен? Пользователь получает ошибку. Аккаунт создан, но он об этом не знает — не получил письмо с подтверждением. Можно добавить retry — но тогда пользователь ждёт несколько секунд пока вы делаете повторные попытки. Плохо.

Что такое очередь задач

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

Хорошая аналогия — касса в супермаркете. Когда вы пробиваете товар, кассир не звонит немедленно поставщику за новой партией. Она просто фиксирует продажу, и склад потом сам разбирается с пополнением запасов. Касса не ждёт поставщика — она работает дальше.

Пользователь регистрируется

Ваш сервер: создать аккаунт + добавить задачу в очередь

Ответ пользователю: «всё готово» (быстро!)

↓ (в фоне, отдельный процесс)

Воркер берёт задачу из очереди

Воркер отправляет письмо

↓ (если не получилось)

Воркер повторяет попытку через N секунд

Что даёт очередь

Надёжность. Задача не потеряется. Если воркер упал — при перезапуске он возьмёт незавершённые задачи и продолжит.

Скорость ответа. Пользователь не ждёт, пока письмо реально отправится. Он получает ответ немедленно.

Контроль нагрузки. Можно ограничить скорость обработки. Если email-сервис принимает 10 писем в секунду — воркер будет отправлять именно с такой скоростью, не больше.

Retry из коробки. Хорошие библиотеки очередей умеют автоматически повторять задачу при ошибке с экспоненциальной задержкой.

Пример с BullMQ (Node.js)

BullMQ — популярная библиотека очередей для Node.js, использует Redis как хранилище задач.

npm install bullmq ioredis

// queue.js — настройка очереди

const { Queue, Worker } = require('bullmq');

const Redis = require('ioredis');

const connection = new Redis(process.env.REDIS_URL);

// Создаём очередь

const emailQueue = new Queue('emails', { connection });

// Добавляем задачу — вызывается при регистрации

async function scheduleWelcomeEmail(userEmail, userName) {

await emailQueue.add(

'welcome', // название задачи

{ email: userEmail, name: userName }, // данные

{

attempts: 5, // максимум 5 попыток

backoff: {

type: 'exponential', // задержка нарастает

delay: 2000, // начиная с 2 секунд

},

}

);

console.log(`Задача на письмо для ${userEmail} добавлена в очередь`);

}

// Воркер — отдельный процесс, который обрабатывает задачи

const worker = new Worker('emails', async (job) => {

console.log(`Отправляем письмо: ${job.data.email}`);

await sendEmail(job.data.email, job.data.name);

console.log(`Письмо отправлено: ${job.data.email}`);

}, { connection });

worker.on('failed', (job, error) => {

console.error(`Задача ${job.id} не удалась: ${error.message}`);

});

module.exports = { scheduleWelcomeEmail };

// server.js — эндпоинт регистрации

const { scheduleWelcomeEmail } = require('./queue');

app.post('/register', async (req, res) => {

// Создаём пользователя

const user = await createUser(req.body);

// Добавляем в очередь — не ждём отправки!

await scheduleWelcomeEmail(user.email, user.name);

// Отвечаем пользователю немедленно

res.json({ ok: true, userId: user.id });

});

Обратите внимание: scheduleWelcomeEmail — это быстрая операция (просто запись в Redis). Пользователь получает ответ за миллисекунды. Письмо уйдёт чуть позже, в фоне.

Когда нужна очередь, а когда нет

Очередь нужна когда:

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

Очередь не нужна когда:

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

Circuit Breaker: автоматический предохранитель

Проблема без circuit breaker

Представьте, что API платёжной системы лёг на два часа. У вас настроен retry: три попытки с паузами. Каждый запрос пользователя занимает теперь ~15 секунд (три попытки с нарастающими паузами) — и всё равно заканчивается ошибкой.

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

Что такое circuit breaker

Circuit breaker дословно — «автоматический выключатель». Это тот же предохранитель, что стоит в электрощитке. Пока ток нормальный — всё работает. Когда пошло короткое замыкание — предохранитель срабатывает и отключает цепь, не давая сгореть всей проводке.

В программировании circuit breaker следит за ошибками при обращении к API. Если ошибок стало слишком много — он «выбивает» и временно прекращает отправлять запросы к проблемному сервису. Через некоторое время — пробует снова, и если всё хорошо — возвращается к нормальной работе.

Три состояния

Circuit breaker работает как переключатель с тремя положениями:

CLOSED (замкнут) — нормальная работа. Запросы проходят свободно. Circuit breaker считает ошибки. Пока их мало — ничего не происходит.

OPEN (разомкнут) — защита активирована. Ошибок стало слишком много. Circuit breaker больше не пропускает запросы к API — сразу возвращает ошибку, не тратя время на попытку. Пользователь получает быстрый ответ «сервис временно недоступен» вместо ожидания 15 секунд.

HALF-OPEN (полуоткрыт) — проверка. Прошло достаточно времени. Circuit breaker пускает один пробный запрос. Если получилось — переходит обратно в CLOSED. Если нет — снова OPEN.

Слишком много ошибок

CLOSED ─────────────────────→ OPEN

↑ │

│ Пробный запрос │ Истекло время ожидания

│ прошёл успешно ↓

└──────────────────────── HALF-OPEN

Аналогия для понимания

Представьте, что вы звоните в службу поддержки. Три раза подряд слышите «все операторы заняты» и трубку кладут. На четвёртый раз вы понимаете: сейчас там явно что-то случилось, не стоит продолжать звонить каждые 30 секунд. Вы решаете перезвонить через час.

Через час звоните — дозвонились. Circuit breaker работает по той же логике, только автоматически.

Пример circuit breaker (Node.js)

// lib/circuit-breaker.js

class CircuitBreaker {

constructor(options = {}) {

// После скольких ошибок подряд «выбить» предохранитель

this.failureThreshold = options.failureThreshold || 5;

// Сколько миллисекунд ждать перед пробным запросом

this.recoveryTimeout = options.recoveryTimeout || 60_000; // 1 минута

// Внутреннее состояние

this.failures = 0; // счётчик ошибок подряд

this.state = 'CLOSED'; // текущее состояние

this.nextAttemptTime = null; // когда можно попробовать снова

}

async call(fn) {

// Если OPEN — проверяем, не пора ли попробовать снова

if (this.state === 'OPEN') {

if (Date.now() < this.nextAttemptTime) {

// Ещё рано — возвращаем ошибку сразу, без запроса

const waitSec = Math.round((this.nextAttemptTime - Date.now()) / 1000);

throw new Error(`Сервис временно недоступен. Повтор через ${waitSec}с`);

}

// Время вышло — переходим в HALF-OPEN для пробного запроса

this.state = 'HALF-OPEN';

console.log('Circuit Breaker: пробный запрос...');

}

try {

const result = await fn();

this._onSuccess();

return result;

} catch (error) {

this._onFailure();

throw error;

}

}

_onSuccess() {

// Запрос прошёл — сбрасываем счётчик, возвращаемся в норму

if (this.state === 'HALF-OPEN') {

console.log('Circuit Breaker: сервис восстановлен, переходим в CLOSED');

}

this.failures = 0;

this.state = 'CLOSED';

}

_onFailure() {

this.failures++;

if (this.state === 'HALF-OPEN') {

// Пробный запрос не удался — снова блокируем

console.warn('Circuit Breaker: пробный запрос не удался, снова OPEN');

this.state = 'OPEN';

this.nextAttemptTime = Date.now() + this.recoveryTimeout;

} else if (this.failures >= this.failureThreshold) {

// Превысили порог ошибок — выбиваем предохранитель

console.error(`Circuit Breaker: ${this.failures} ошибок подряд — переходим в OPEN`);

this.state = 'OPEN';

this.nextAttemptTime = Date.now() + this.recoveryTimeout;

}

}

}

module.exports = { CircuitBreaker };

// Использование

const { CircuitBreaker } = require('./lib/circuit-breaker');

// Создаём один breaker на весь сервис (не на каждый запрос!)

const paymentBreaker = new CircuitBreaker({

failureThreshold: 5, // 5 ошибок подряд — выбить

recoveryTimeout: 60_000 // через 1 минуту — пробный запрос

});

async function processPayment(orderData) {

try {

return await paymentBreaker.call(() =>

paymentApi.post('/charge', orderData)

);

} catch (error) {

if (error.message.includes('временно недоступен')) {

// Circuit breaker сработал — API явно лежит

// Можно добавить задачу в очередь на потом

await paymentQueue.add('retry_payment', orderData, {

delay: 5 * 60_000 // попробовать через 5 минут

});

return { status: 'queued', message: 'Оплата будет обработана позже' };

}

throw error;

}

}

Как всё это работает вместе

Три инструмента не конкурируют — они дополняют друг друга и закрывают разные сценарии.

Retry — для коротких временных сбоев. API чихнул и через 2 секунды ожил. Retry справится сам, пользователь даже не заметит.

Circuit Breaker — для затяжных сбоев. API лежит час. Без circuit breaker каждый запрос будет ждать таймаутов и retry по 15 секунд, убивая ресурсы сервера. С ним — сразу быстрый ответ «недоступно», нагрузка не накапливается.

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

Запрос к API

[Circuit Breaker]

OPEN? ──→ сразу «недоступно» → добавить в очередь

CLOSED/HALF-OPEN ↓

[Retry с задержкой]

Успех ──→ вернуть результат

Ошибка после всех попыток ──→ сообщить circuit breaker

└→ если не срочно — добавить в очередь

Что показать пользователю когда ничего не помогло

Это важная часть, которую часто забывают. Если все попытки исчерпаны — пользователь должен получить понятный ответ, а не «Internal Server Error».

Плохо:

500 Internal Server Error

Хорошо:

Сервис оплаты временно недоступен. Ваш заказ сохранён —

мы обработаем оплату автоматически в течение 15 минут

и пришлём подтверждение на email.

Разница огромная. Первый ответ пугает и непонятен. Второй — честный, объясняет что произошло и что будет дальше.

Если задача добавлена в очередь — скажите об этом:

app.post('/send-report', async (req, res) => {

try {

// Пробуем сделать сразу

const result = await reportService.generate(req.body);

return res.json({ status: 'done', url: result.url });

} catch (error) {

// Не получилось — добавляем в очередь

const jobId = await reportQueue.add('generate', req.body);

return res.json({

status: 'queued',

message: 'Отчёт формируется. Мы пришлём его на email когда будет готов.',

jobId

});

}

});

Готовые библиотеки: не нужно писать самому

Circuit breaker и retry — стандартные паттерны, для них есть проверенные библиотеки.

Язык Библиотека Что умеет Node.js cockatiel Circuit breaker, retry, timeout — всё вместе Node.js opossum Circuit breaker с метриками Node.js async-retry Простой retry с нарастающей задержкой Python tenacity Retry с гибкими правилами Python circuitbreaker Circuit breaker декоратором Node.js (очереди) BullMQ Очереди на Redis с retry из коробки Python (очереди) Celery Очереди задач с поддержкой Redis и RabbitMQ

Пример с cockatiel — он реализует всё из этой статьи в нескольких строках:

const { Policy, ConsecutiveBreaker, ExponentialBackoff } = require('cockatiel');

// Создаём политику: circuit breaker + retry

const policy = Policy

.wrap(

// Circuit breaker: выбить после 5 ошибок подряд, восстановление через 30 сек

Policy.handleAll().circuitBreaker(30_000, new ConsecutiveBreaker(5)),

// Retry: 3 попытки с экспоненциальной задержкой

Policy.handleAll().retry().attempts(3).exponential()

);

// Использование — просто оборачиваете любой вызов

const data = await policy.execute(() => api.get('/data'));

Чеклист

Когда добавлять retry:

☐ Запрос может временно не пройти по сети

☐ API иногда возвращает 429 или 500

☐ Добавлена нарастающая задержка (не фиксированная)

☐ Добавлен jitter (случайный сдвиг)

☐ Ретраятся только временные ошибки (не 400, 401, 404)

Когда добавлять очередь:

☐ Результат не нужен пользователю прямо сейчас

☐ Задача может занять больше 2–3 секунд

☐ Нельзя потерять задачу при перезапуске сервера

☐ Нужно контролировать скорость выполнения

Когда добавлять circuit breaker:

☐ API критичен и может надолго лечь

☐ Много параллельных запросов к одному сервису

☐ Хочется быстро отвечать «недоступно» вместо ожидания таймаутов

Что показывать пользователю:

☐ Понятное сообщение, а не технический код ошибки

☐ Если задача в очереди — сообщить об этом

☐ Если возможно — дать примерное время ожидания

Итог

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

Три инструмента из этой статьи покрывают большинство сценариев. Retry — для мелких кратковременных сбоев. Очередь — чтобы не терять задачи и не держать пользователя у экрана. Circuit Breaker — чтобы затяжной сбой в одном сервисе не утянул за собой весь ваш сервер.

Начать можно с малого: добавить retry на самые важные внешние вызовы. Потом — очередь для фоновых задач вроде писем и уведомлений. Circuit breaker — когда видите, что один упавший API начинает влиять на работу всего приложения.

FAQ

Нужны ли все три инструмента сразу? Нет. Начните с retry — это даст 80% надёжности за 20% усилий. Очередь добавляйте когда появляются фоновые задачи (письма, уведомления, отчёты). Circuit breaker — когда замечаете, что сбой одного API влияет на работу всего сервиса.

Что использовать для хранения очереди задач? Для большинства проектов достаточно Redis + BullMQ (Node.js) или Redis + Celery (Python). Redis — быстрый, надёжный, и вы, скорее всего, уже используете его для кэша или сессий.

Сколько попыток делать в retry? Обычно 3–5 достаточно. Больше — редко имеет смысл: если API не ответил за 5 попыток, значит проблема не временная. Для критичных операций лучше добавить задачу в очередь, чем делать 10 попыток подряд.

Circuit breaker срабатывает слишком часто — что настроить? Увеличьте failureThreshold — количество ошибок подряд до срабатывания. Или уменьшите recoveryTimeout — время до пробного запроса. Оптимальные значения зависят от конкретного API: как часто он бывает нестабилен и как долго обычно восстанавливается.

Источник и полная версия: VibeCode Wiki