# How do I set up a Circuit Breaker for a flaky third-party API dependency?

> A breaker alone won't hold: pull `connect_timeout` and `timeout` down to 800ms-2s, isolate the dependency behind a bulkhead, and define a cached fallback.

- Asked: 2026-05-25
- Answered: 2026-05-28
- Asked by: Tunç
- Tags: dayaniklilik, mimari, laravel
- Source: https://www.muhammetsafak.com.tr/en/just-ask/circuit-breaker-for-flaky-third-party-apis/
- Language: en-US
- Author: Muhammet Şafak

---
**Question:** Our app fetches shipping prices from an external carrier API. When the carrier slows down or goes down, our requests hit the timeout, the Octane/PHP-FPM workers block, and the whole system locks up.

How do I set up a Circuit Breaker with Guzzle middleware or a Go library to stop this cascade? How should the system behave in the Open/Closed/Half-Open states?


Short answer: a circuit breaker alone won't save you — it saves you when you pair it with an **aggressive timeout**. Because what kills your workers isn't the error itself, it's the 30 seconds of waiting that lead up to it.

What you're hitting is a classic **cascading failure**: a slow dependency makes every request wait on the timeout, the pool fills, the system locks. The breaker's job is to stop calling a failing dependency entirely.

1. **Get the three states right.** `CLOSED`: calls pass, failures are counted. Once the threshold trips, go `OPEN`: don't call the dependency at all, return a fallback immediately, and stay that way for a cooldown. When the cooldown ends, go `HALF-OPEN`: let a few trial calls through — on success go back to `CLOSED`, on failure flip back to `OPEN`. That's the heart of it.
2. **Cut the timeout first — that's the real killer.** Keep `connect_timeout` and `timeout` in milliseconds, not seconds (e.g. 800ms-2s). A breaker is useless without a short timeout; set both or the pool fills anyway.
3. **Isolate that dependency with a bulkhead.** Cap the number of concurrent requests allowed to the carrier API. That way, when it slows down it consumes only a small slice reserved for it, not all your workers, and the core path (cart, checkout) stays up.
4. **Define the fallback in advance.** Return the last cached shipping price, and if there's none, say "estimate unavailable right now." Serve a degraded-but-working experience instead of an error page.
5. **Pick the right tool.** In Laravel wrap the Guzzle client in middleware and keep the state in Redis. In Go don't hand-roll it — `sony/gobreaker` already gives you these three states.

**Bottom line:** I'd never leave the breaker alone: I set it up as a trio of short timeout + bulkhead + cached fallback. Workers blocking while they wait on a timeout hits especially hard in a persistent-process architecture (Octane), so cut the timeout first, then add the breaker.

## Related Reading

- [Laravel Octane: the performance that comes with a persistent process](/en/blog/laravel-octane-persistent-process-performance/) — Blog
- [How do I handle poison-pill messages and a Dead Letter Queue in the queue?](https://www.muhammetsafak.com.tr/en/just-ask/poison-pill-messages-and-dead-letter-queues-in-laravel/) — Just Ask
- [Graceful degradation: how do I isolate non-critical services under load?](https://www.muhammetsafak.com.tr/en/just-ask/graceful-degradation-isolating-non-critical-services-under-load/) — Just Ask
- [Should I use chunkById instead of chunk when the same job also updates the rows it iterates?](https://www.muhammetsafak.com.tr/en/just-ask/should-i-use-chunkbyid-instead-of-chunk-when-the-same-job/) — Just Ask
