Circuit breaker para APIs externas com Laravel Http Client e Redis
Por John Pires · 29 de março de 2026

Circuit breaker para APIs externas com Laravel Http Client e Redis
Cenario pratico do dia a dia
Problema real: chamadas externas falhando
Você tem um serviço Laravel que consome uma API externa (pagamentos, CEP, validação de documentos). Quando a API externa começa a falhar, suas requisições disparam erros, filas enchem, latência sobe e a aplicação inteira degrada. Isso acontece frequentemente em produção e os desenvolvedores colocam retry simples com sleep em qualquer lugar — o problema piora.
Impacto em sistemas distribuídos
Em uma arquitetura com múltiplas instâncias do Laravel (horizontal scaling), um flag em memória não resolve: uma instância pode bloquear chamadas enquanto outra continua massacrando o endpoint externo. Precisamos de um estado compartilhado com consistência suficiente para tomar decisões rápidas: é aqui que Redis entra como a camada de coordenação.
Este cenário assume: Laravel 10+, PHP 8.3+, Redis como provider central, e uso do Http Client do Laravel (Illuminate\Support\Facades\Http).
Tip: circuit breaker não é só evitar chamadas; é proteger capacidade do seu sistema e permitir recuperação controlada.
Arquitetura da solucao
Estado e transições (CLOSED, OPEN, HALF_OPEN)
O padrão define três estados: CLOSED (tudo normal), OPEN (não tentar a API por um período) e HALF_OPEN (permitir algumas tentativas para verificar se o serviço voltou). Implementaremos essas transições com chaves Redis e operações atômicas.
Onde a lógica vive — borda vs core
Colocar a lógica do circuit breaker em todos os lugares é erro. A solução ideal centraliza a lógica em um serviço (por exemplo, App\Services\CircuitBreakerService) e expõe uma API simples: allowRequest(), recordSuccess(), recordFailure(). Chamadas externas passam por esse serviço.
Na prática, teremos três componentes:
- Classe CircuitBreaker (controle em Redis)
- Wrapper HTTP que integra com Http:: (faz checagem antes de cada chamada)
- Fallbacks/handlers para casos em que Redis fica inacessível
Codigo completo com explicacoes
Jeito errado: implementação ad-hoc na controller
Exemplo comum: catch/try com flag em memória e retries com sleep. Funciona localmente, rompe em produção.
Contexto: este código é encontrado em controllers e cria problemas em múltiplas instâncias e quando a falha é prolongada.
<?php
// controllers/PaymentController.php (errado)
// Faz retries inline e seta uma flag em memória
namespace App\Http\Controllers;
use Illuminate\Support\Facades\Http;
use Illuminate\Http\Request;
class PaymentController extends Controller
{
private bool $externalDown = false; // apenas em memória
public function charge(Request $request)
{
if ($this->externalDown) {
return response()->json(['error' => 'external service down'], 503);
}
try {
for ($i = 0; $i < 3; $i++) {
$resp = Http::timeout(2)->post('https://api.example.com/charge', $request->all());
if ($resp->successful()) {
return $resp->json();
}
sleep(1);
}
} catch (\Exception $e) {
$this->externalDown = true; // só para a instância atual
return response()->json(['error' => 'request failed'], 502);
}
return response()->json(['error' => 'max retries reached'], 502);
}
}
O que acontece depois: quando a API externa cai, cada instância faz retries e aumenta carga na API. Flag em memória não impede outras instâncias.
Jeito certo: CircuitBreaker centralizado com Redis
Vamos implementar uma classe que usa Redis para gerenciar estado, um wrapper para chamadas Http e um ServiceProvider opcional para injeção. A ênfase é atomicidade e baixo número de chamadas Redis por operação.
Contexto: classe usa keys por nome de serviço e mantém contador de falhas, estado e timestamps. Usa Lua script para transições atômicas.
<?php
// app/Services/CircuitBreaker.php (trecho principal)
namespace App\Services;
use Illuminate\Support\Facades\Redis;
use Carbon\Carbon;
class CircuitBreaker
{
private string $name;
private int $failureThreshold;
private int $openTimeout; // seconds
private int $halfOpenMaxRequests;
public function __construct(string $name, int $failureThreshold = 5, int $openTimeout = 60, int $halfOpenMaxRequests = 2)
{
$this->name = $name;
$this->failureThreshold = $failureThreshold;
$this->openTimeout = $openTimeout;
$this->halfOpenMaxRequests = $halfOpenMaxRequests;
}
private function key(string $suffix): string
{
return "cb:{$this->name}:{$suffix}";
}
public function allowRequest(): bool
{
$state = Redis::get($this->key('state')) ?: 'CLOSED';
if ($state === 'OPEN') {
$openedAt = (int) Redis::get($this->key('opened_at'));
if ($openedAt + $this->openTimeout <= time()) {
// transit to HALF_OPEN
Redis::set($this->key('state'), 'HALF_OPEN');
Redis::del($this->key('half_open_successes'));
Redis::del($this->key('half_open_attempts'));
return true;
}
return false;
}
if ($state === 'HALF_OPEN') {
$attempts = (int) Redis::incr($this->key('half_open_attempts'));
if ($attempts > $this->halfOpenMaxRequests) {
// limit concurrent half-open attempts
Redis::decr($this->key('half_open_attempts'));
return false;
}
return true;
}
// CLOSED
return true;
}
public function recordSuccess(): void
{
$state = Redis::get($this->key('state')) ?: 'CLOSED';
if ($state === 'HALF_OPEN') {
$successes = Redis::incr($this->key('half_open_successes'));
if ($successes >= $this->halfOpenMaxRequests) {
// close the breaker
Redis::set($this->key('state'), 'CLOSED');
Redis::del($this->key('failures'));
Redis::del($this->key('half_open_attempts'));
Redis::del($this->key('half_open_successes'));
}
return;
}
// CLOSED -> reset failures on success
Redis::del($this->key('failures'));
}
public function recordFailure(): void
{
$failures = (int) Redis::incr($this->key('failures'));
if ($failures === 1) {
// set an expiry to count windowed failures
Redis::expire($this->key('failures'), $this->openTimeout);
}
if ($failures >= $this->failureThreshold) {
// open the circuit
Redis::set($this->key('state'), 'OPEN');
Redis::set($this->key('opened_at'), time());
}
}
}
O que acontece depois: controle central de estado em Redis, múltiplas instâncias respeitam o mesmo estado e evitam tiros de misericórdia à API. A implementação acima é simples, explícita e suficiente para muitas necessidades.
Integrando com Laravel Http Client
Vamos expor um helper que usa o CircuitBreaker para proteger chamadas. Ele chama allowRequest() antes do Http:: e registra sucesso/falha após a resposta/exception.
<?php
// app/Services/ExternalHttpClient.php
namespace App\Services;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
class ExternalHttpClient
{
private CircuitBreaker $cb;
public function __construct(CircuitBreaker $cb)
{
$this->cb = $cb;
}
public function post(string $url, array $payload)
{
if (! $this->cb->allowRequest()) {
// fallback imediato: pode ser queue, cache, ou erro controlado
return response()->json(['error' => 'service temporarily unavailable'], 503);
}
try {
$resp = Http::timeout(5)->post($url, $payload);
if ($resp->successful()) {
$this->cb->recordSuccess();
return $resp;
}
// 4xx/5xx contam como falha
$this->cb->recordFailure();
return $resp;
} catch (\Exception $e) {
Log::warning('External call failed: '.$e->getMessage());
$this->cb->recordFailure();
throw $e; // ou retornar fallback
}
}
}
Contexto pós-código: este wrapper centraliza retries e circuit breaker, facilita testes e permite trocar estratégias sem mudar controllers.
Tratamento de erros e edge cases
Redis indisponível — como proceder
Se Redis falhar, o circuit breaker perde a fonte de verdade. Escolha um modo de degradação: fail-open (permitir chamadas) ou fail-closed (bloquear todas). Recomendo fail-open com limites de tempo e coleta de métricas, porque fail-closed pode derrubar serviço inteiro se Redis estiver temporariamente fora.
Condições de corrida e atomicidade
Operações como incrementar contador e verificar threshold devem ser atômicas. Em implementações simples usamos INCR + EXPIRE; isso é aceitável mas tem janelas. Para produção sensível, use Lua scripts no Redis para executar lógica composta em um único round-trip.
<?php
// exemplo de Lua script executado via Redis::eval para atomically incrementar e abrir circuit
$lua = <<<'LUA'
local failures_key = KEYS[1]
local state_key = KEYS[2]
local opened_key = KEYS[3]
local threshold = tonumber(ARGV[1])
local open_timeout = tonumber(ARGV[2])
local failures = redis.call('INCR', failures_key)
if failures == 1 then
redis.call('EXPIRE', failures_key, open_timeout)
end
if failures >= threshold then
redis.call('SET', state_key, 'OPEN')
redis.call('SET', opened_key, tonumber(redis.call('TIME')[1]))
end
return failures
LUA;
$result = Redis::eval($lua, 3, 'cb:payments:failures', 'cb:payments:state', 'cb:payments:opened_at', 5, 60);
var_dump($result);
Explicação: o script faz INCR + EXPIRE e seta estado OPEN com timestamp em um único passo, evitando condições de corrida entre comandos separados.
Half-open com concorrência
Quando muitos processos entram em HALF_OPEN ao mesmo tempo, você precisa limitar tentativas (halfOpenMaxRequests) e/ou usar tokens. O código acima usa contador 'half_open_attempts' para limitar paralelismo.
Erros comuns:
- Esquecer de limpar counters quando reabrir
- Contar 4xx como sucesso — defina claramente quais códigos contam como falha
- Ignorar latências altas que também podem indicar degradação
Performance e otimizacao
Reduzir chamadas Redis
Evite múltiplos comandos Redis por request. Use pipeline quando possível ou scripts Lua para agrupar lógica. Também armazene chaves com TTL apropriado para não poluir namespace.
Estratégias de retry e backoff
Retry indiscriminado é perigoso. Se decidir retry, use backoff exponencial com jitter e contador máximo. Melhor alternativa: deixe o circuit breaker controlar quando retriar através do HALF_OPEN.
Exemplo: retry local com backoff (se usar):
<?php
// exemplo de retry simples com backoff exponencial e jitter
$attempt = 0;
$max = 3;
while ($attempt < $max) {
try {
$resp = Http::timeout(3)->post('https://api.example.com/endpoint', $data);
if ($resp->successful()) break;
} catch (\Exception $e) {
// log
}
$attempt++;
$sleep = (int) (pow(2, $attempt) + rand(0, 100) / 100.0);
usleep($sleep * 1000 * 1000);
}
Contexto após o código: retries devem ser pequenos e condicionais; use o circuit breaker para decidir se vale a pena tentar novamente.
Monitoramento e métricas
Não basta proteger chamadas; monitore: contagem de falhas, tempo médio de resposta, taxa de abertura do circuito. Exporte métricas para Prometheus/StatsD. Alarmes para estados OPEN persistentes são essenciais.
Conclusao pratica
Implementar circuit breaker com Redis e Laravel Http Client reduz riscos de cascata e torna seu consumo de APIs externas previsível. Evite soluções ad-hoc em memória; prefira estado compartilhado e operações atômicas.
Checklist mínimo para implementar em produção:
- Classe centralizada CircuitBreaker com Redis
- Wrapper HTTP que faz allowRequest/recordSuccess/recordFailure
- Usar scripts Lua quando precisar de atomicidade entre operações
- Limitar concorrência em HALF_OPEN
- Degradação segura quando Redis estiver indisponível
- Métricas e alertas
Erros de implementação aumentam latência e podem derrubar terceiros — esse artigo mostrou o caminho errado e o certo com exemplos práticos em PHP/Laravel. Aplique essas práticas, escreva testes integrados e monitore após o deploy.
Implementar circuit breaker não é rocket science, mas requer disciplina: centralizar lógica, tratar edge cases e instrumentar. Faça code reviews focados nesses pontos e prefira implementações simples e observáveis.
Assuntos: Laravel, PHP, NoSQL, http-client, circuit-breaker, best-practices
Leia também
ETags e cache condicional em APIs Laravel (PHP 8.3): reduzir banda e latência
Como implementar ETags e cache condicional em APIs Laravel (PHP 8.3) para reduzir banda e latência: patterns práticos, middleware, fingerprint por coleção e…
Filament: importar CSV com jobs na fila e mostrar progresso em Livewire
Tutorial prático para iniciantes: configure fila no Laravel, crie upload no Filament, dispare um Job que processa CSV e atualize um componente Livewire com…
Laravel 10: consumir APIs externas com Http Client, retries, fallback e cache
Guia prático para iniciantes em Laravel 10 sobre como consumir APIs externas usando Http Client: timeout, retries, cache e fallback, com exemplos errados e…
Autenticação de API com Laravel Sanctum para SPA e mobile — passo a passo
Guia prático para autenticar SPA e apps mobile com Laravel Sanctum: compare o jeito errado (token no localStorage) com o certo (cookies para SPA, tokens com…
APIs type-safe no Laravel: DTOs tipados, Value Objects e validação centralizada
Implementação prática para APIs type-safe em Laravel: DTOs readonly, Value Objects (Email, Money), validação centralizada com FormRequest e resolução…
Webhooks seguros no Laravel: HMAC, idempotência e teste com ngrok
Como proteger endpoints de webhook no Laravel: validar HMAC, garantir idempotência com tabela e unique constraint, e testar localmente com ngrok. Guia prático…