Надёжные интеграции: retry и fallback при работе с внешними API
Любая интеграция с внешним API — это контракт без гарантий. Сервис может ответить 500, отдать таймаут, вернуть мусор или просто молчать. На проде это значит потерянные продажи и недовольных клиентов.
Базовые проблемы
- Сеть нестабильна — таймауты, обрывы соединений.
- API провайдера падает или деплоится — 5xx ошибки.
- Лимиты — 429 Too Many Requests.
- Семантические ошибки — провайдер ответил «успех», но реально операция не прошла.
Хорошая интеграция строится не на «надеемся, что заработает», а на предположении, что внешний сервис ненадёжен.
Retry: повторные попытки
Простейший приём — повторить запрос, если он не удался. Но повторять нужно умно.
Что повторяем: сетевые ошибки, таймауты, 5xx, 429. Что НЕ повторяем: 4xx (кроме 408 и 429). Если API сказал «неверные данные» — повторение не поможет.
return retry(
times: 3,
callback: fn () => Http::timeout(5)->post($url, $payload),
sleepMilliseconds: fn (int $attempt) => $attempt * 1000,
when: fn (Throwable $e) => $e instanceof ConnectionException
|| ($e instanceof RequestException && $e->response->status() >= 500),
);
Важная деталь — экспоненциальный backoff. Если сервис упал, тысяча клиентов одновременно начнут долбить его при первой же попытке восстановления и положат снова. Backoff + jitter (случайное смещение) спасают.
Fallback: запасной путь
Если у вас несколько провайдеров одного сервиса (SMS, платежи, email) — на ошибке основного переключайтесь на резервный.
foreach ($this->providers as $provider) {
try {
return $provider->send($message);
} catch (ProviderUnavailableException $e) {
Log::warning('Provider failed, fallback', ['provider' => $provider->name()]);
continue;
}
}
throw new AllProvidersFailedException();
В реальном SMS-шлюзе это спасает: один провайдер начал тормозить — трафик автоматически идёт через второго.
Идемпотентность
Главный риск ретраев: операция могла выполниться, а ответ — потеряться. Повторив запрос, мы спишем деньги дважды.
Решение — идемпотентный ключ. Генерируем UUID на стороне клиента и шлём вместе с запросом. Сервер на этот ключ может выполнить операцию ровно один раз.
Если внешний API не поддерживает идемпотентность — храните «уже отправлено» у себя в БД и проверяйте перед каждым ретраем.
Что ещё делает интеграции живучими
- Circuit breaker. После N подряд ошибок — временно перестаём бить провайдера, даём ему передохнуть.
- Таймауты. На каждый внешний вызов. Без таймаута воркер зависает на 10 минут на одном запросе.
- Метрики. Latency, error rate, retry count — в Prometheus или Grafana. Без метрик вы узнаете о проблеме от клиентов, а не от мониторинга.
- Логирование запроса/ответа. При разборе инцидента это спасает часы времени.
Запомнить
- Внешний сервис ВСЕГДА может упасть. Проектируйте интеграции из этого предположения.
- Retry — только для повторяемых ошибок, обязательно с backoff.
- Fallback между провайдерами — лучший способ повысить аптайм.
- Идемпотентность — обязательна для денежных и других «несимметричных» операций.
- Метрики и логи — глаза и уши интеграции.