Retries в Go: почему retry 3 times может добить сервис
Представим сервис, который обращается к внешнему API. Всё работает нормально, пока API временно не начинает отвечать: 503 Service Unavailable Что часто делают? for i := 0; i < 3; i++ { err := request() if err == nil { break } } Логика понятная: ошибка временная → попробуем ещё раз. Но представим: 1000 requests ↓ external API ↓ 503 Все 1000 запросов начинают повторяться. В итоге: 1000 original requests + 3000 retries ↓ 4000 requests Внешний сервис всё ещё перегружен, а retry только увеличивают нагрузку. Так временный сбой может превратиться в каскадный отказ. Что делать? Не повторять запрос сразу. Вместо: request ↓ error ↓ retry ↓ error ↓ retry используют exponential backoff: retry 1 → 100 ms retry 2 → 200 ms retry 3 → 400 ms retry 4 → 800 ms Но и здесь есть проблема. Если 1000 клиентов получили ошибку одновременно, они могут одновременно выполнить retry: 1000 clients ↓ wait 400 ms ↓ 1000 retries Для этого добавляют jitter - небольшую случайную задержку: client 1 → 327 ms client 2 → 391 ms client 3 → 442 ms client 4 → 365 ms ... Теперь повторные запросы распределяются во времени, а не приходят одной волной. Retry подходит не для любой ошибки Повторить запрос часто имеет смысл при: 503 timeout connection reset А вот эти ошибки обычно повторять бессмысленно: 400 Bad Request 401 Unauthorized 403 Forbidden Ещё важно учитывать идемпотентность операции. Повторить: GET /users/123 обычно безопаснее, чем: POST /payments Повторный POST может создать второй платёж или повторить другое побочное действие, если API не поддерживает идемпотентность. Поэтому хороший retry - это не: "если ошибка → повторить 3 раза" А скорее: error ↓ retryable? ↓ yes backoff + jitter ↓ retry ↓ limit attempts / timeout В production retry обычно должен учитывать: максимальное количество попыток; общий timeout или deadline; exponential backoff; jitter; список retryable-ошибок; идемпотентность операции. Retry должен помогать пережить временный сбой, а не превращать один сбой в лавину новых запросов.