Your queue workers are burning through jobs at full speed, retrying a third-party API endpoint that’s been down for three hours. Every retry fails. Every failure generates a Sentry alert. You’re 55,000 errors deep, your queue is backed up, and the external service doesn’t care how many times you knock.
This is what happens when you don’t have a circuit breaker.
The Pattern
A circuit breaker sits between your application and an unreliable external service. It tracks failures and, after a threshold, stops sending requests entirely for a cooldown period. The metaphor comes from electrical engineering — when there’s too much current, the breaker trips to prevent damage.
Three states:
- Closed — everything works normally, requests flow through
- Open — too many failures, all requests short-circuit immediately (return error without calling the API)
- Half-Open — after cooldown, let one request through to test if the service recovered
A Simple Implementation
class CircuitBreaker
{
public function __construct(
private string $service,
private int $threshold = 5,
private int $cooldownSeconds = 300,
) {}
public function isAvailable(): bool
{
$failures = Cache::get("circuit:{$this->service}:failures", 0);
$openedAt = Cache::get("circuit:{$this->service}:opened_at");
if ($failures < $this->threshold) {
return true; // Closed state
}
if ($openedAt && now()->diffInSeconds($openedAt) > $this->cooldownSeconds) {
return true; // Half-open: try one request
}
return false; // Open: reject immediately
}
public function recordFailure(): void
{
$failures = Cache::increment("circuit:{$this->service}:failures");
if ($failures >= $this->threshold) {
Cache::put("circuit:{$this->service}:opened_at", now(), $this->cooldownSeconds * 2);
}
}
public function recordSuccess(): void
{
Cache::forget("circuit:{$this->service}:failures");
Cache::forget("circuit:{$this->service}:opened_at");
}
}
Using It in a Queue Job
class FetchWeatherDataJob implements ShouldQueue
{
public function handle(WeatherApiClient $client): void
{
$breaker = new CircuitBreaker('weather-api', threshold: 5, cooldownSeconds: 300);
if (! $breaker->isAvailable()) {
// Release back to queue for later
$this->release(60);
return;
}
try {
$response = $client->getConditions($this->stationId);
$breaker->recordSuccess();
$this->storeWeatherData($response);
} catch (ApiException $e) {
$breaker->recordFailure();
throw $e; // Let Laravel's retry handle it
}
}
}
Pair It With Exponential Backoff
Circuit breakers prevent hammering. Exponential backoff spaces out retries. Use both:
class FetchWeatherDataJob implements ShouldQueue
{
public int $tries = 5;
public function backoff(): array
{
return [30, 60, 120, 300, 600]; // 30s, 1m, 2m, 5m, 10m
}
}
When You Need This
If your application integrates with external APIs that can go down — email verification services, geocoding providers, analytics feeds — you need circuit breakers. The symptoms that tell you it’s time:
- Thousands of identical errors in your error tracker from one endpoint
- Queue workers stuck retrying failed jobs instead of processing good ones
- Your application slowing down because every request waits for a timeout
- Rate limit responses (HTTP 429) from the external service
Without a circuit breaker, a flaky external service doesn’t just affect itself — it takes your entire queue infrastructure down with it. Five minutes of setup saves hours of firefighting.
Leave a Reply