Retry с экспоненциальным backoff
Экспоненциальный backoff — стратегия повторных запросов, при которой каждая следующая попытка ждёт вдвое дольше предыдущей, снижая нагрузку на перегруженный сервер.
Зачем нужно
Простой retry в цикле без ожидания при сбое сервера приводит к «шторму запросов»: все клиенты одновременно повторяют запросы, перегружая и без того нестабильный сервис. Экспоненциальный backoff + jitter (случайный разброс) равномерно распределяет повторные запросы во времени и даёт серверу возможность восстановиться.
Где используется
- HTTP-клиенты, обращающиеся к внешним API
- Подключение к базам данных и брокерам сообщений при старте сервиса
- Паттерн Circuit Breaker (в дополнение к backoff)
- AWS SDK, Google Cloud SDK — backoff встроен по умолчанию
Алгоритм
Попытка 1: немедленно
Попытка 2: ждать base * 2^0 = base мс
Попытка 3: ждать base * 2^1 = base*2 мс
Попытка 4: ждать base * 2^2 = base*4 мс
...
Попытка N: ждать min(base * 2^(N-2), maxDelay) мс
+ jitter: ± random(0, delay * 0.25) — разброс между клиентами
Реализация
async function fetchWithRetry(url, options = {}, {
maxRetries = 3,
baseDelay = 500, // мс
maxDelay = 10_000, // мс
retryOn = [429, 502, 503, 504],
} = {}) {
let lastError;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
const res = await fetch(url, options);
// Успех или клиентская ошибка (4xx кроме 429) — не повторяем
if (res.ok || (res.status < 500 && res.status !== 429)) {
return res;
}
// Серверная ошибка или rate limit — повторяем
if (!retryOn.includes(res.status)) return res;
lastError = new Error(`HTTP ${res.status}`);
// Читаем Retry-After из заголовка (приоритет над backoff)
const retryAfter = res.headers.get('Retry-After');
if (retryAfter) {
await sleep(parseInt(retryAfter) * 1000);
continue;
}
} catch (err) {
// Сетевая ошибка (нет соединения, таймаут)
lastError = err;
}
if (attempt < maxRetries) {
const delay = calcDelay(attempt, baseDelay, maxDelay);
console.warn(`Retry ${attempt + 1}/${maxRetries} after ${delay}ms`);
await sleep(delay);
}
}
throw lastError;
}
function calcDelay(attempt, base, max) {
const exponential = base * Math.pow(2, attempt);
const capped = Math.min(exponential, max);
// Jitter: ±25% случайного разброса
const jitter = capped * 0.25 * (Math.random * 2 - 1);
return Math.round(capped + jitter);
}
const sleep = ms => new Promise(r => setTimeout(r, ms));
// Использование
const res = await fetchWithRetry('/api/orders', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ items: [...] }),
});
Идемпотентность и retry
// Безопасно повторять: GET, HEAD, DELETE, PUT (идемпотентные)
// Опасно повторять: POST — может создать дубликаты
// Защита от дублей при retry POST: Idempotency-Key
const idempotencyKey = crypto.randomUUID;
const res = await fetchWithRetry('/api/payments', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Idempotency-Key': idempotencyKey, // сервер дедуплицирует по ключу
},
body: JSON.stringify({ amount: 1000 }),
});
Частые ошибки
- Retry без ожидания — «шторм запросов» при падении сервера
- Одинаковый delay для всех клиентов без jitter — синхронные волны запросов
- Retry на 4xx ошибки (кроме 429) — повторный запрос с теми же данными не поможет
- POST-retry без Idempotency-Key — двойные платежи, дублирование заказов
- Бесконечный retry без maxRetries — зависание приложения
Связанные темы
- _MOC Сеть
- Rate Limiting -- клиентская сторона
- Abort Controller -- отмена запросов
- Протокол HTTP -- основы