Коли сторонній сервіс лежить, повтори й тайм-аути не допомагають, а шкодять: кожен запит чекає повний тайм-аут, воркери й процеси PHP зайняті очікуванням, черга росте, а сервіс, що намагається піднятися, отримує лавину запитів.
Circuit Breaker («запобіжник») відстежує помилки й після порогу перестає викликати сервіс на певний час:
Closed (норма) -- N помилок поспіль --> Open (виклики не йдуть, одразу помилка)
^ |
| минув час
| v
+----- успіх ---- Half-open (пропустити пробний запит) ---- помилка ---> Open
Що це дає:
- швидка відмова замість 30-секундного очікування: користувач одразу бачить запасний варіант, воркери вільні;
- сервісу дають відновитися без лавини повторів;
- запасна поведінка: закешовані дані, відкладена обробка, повідомлення «сервіс тимчасово недоступний».
У Laravel для фонових задач роль запобіжника виконує middleware ThrottlesExceptions:
public function middleware(): array
{
return [
(new ThrottlesExceptions(maxAttempts: 10, decaySeconds: 5 * 60))
->by('payment-provider') // спільно для всіх джоб провайдера
->when(fn (Throwable $e) => $e instanceof ConnectionException
|| ($e instanceof RequestException && $e->response->serverError())),
];
}
Після 10 помилок джоби з цим ключем не виконуються 5 хвилин, а повертаються в чергу. З retryUntil() вони не губляться.
Для синхронних викликів простий запобіжник на кеші:
final class CircuitBreaker
{
public function __construct(private string $service, private int $threshold = 5, private int $cooldown = 60) {}
public function call(callable $callback, callable $fallback): mixed
{
if (Cache::has("circuit:{$this->service}:open")) {
return $fallback();
}
try {
$result = $callback();
Cache::forget("circuit:{$this->service}:failures");
return $result;
} catch (ConnectionException|RequestException $e) {
if (Cache::increment("circuit:{$this->service}:failures") >= $this->threshold) {
Cache::put("circuit:{$this->service}:open", true, $this->cooldown);
}
return $fallback();
}
}
}
Що важливо:
- рахувати лише «системні» помилки (мережа, 5xx, тайм-аути), а не 4xx від ваших же некоректних запитів;
- стан спільний для всіх процесів - у Redis, а не в пам'яті одного воркера;
- сповіщення: перехід у стан Open - подія для моніторингу й чергового;
- короткі тайм-аути - запобіжник не замінює їх, а доповнює.
Готові рішення є й у вигляді пакетів, але для більшості Laravel-застосунків ThrottlesExceptions у черзі + короткі тайм-аути + кеш як запасний варіант покривають потреби.