Imagine que sua aplicação Ruby on Rails dependa de uma API externa para realizar uma operação importante.
Pode ser uma API de pagamento, emissão de nota fiscal, consulta de crédito, envio de mensagens ou qualquer outro serviço necessário para o funcionamento do negócio.
Em condições normais, o fluxo é simples:
Rails → API externa → resposta
Mas o que acontece quando essa API começa a apresentar lentidão ou simplesmente fica fora do ar?
Se a aplicação continuar tentando realizar chamadas indefinidamente, um problema que começou em um serviço externo pode rapidamente consumir recursos da sua própria aplicação.
É nesse cenário que entra o Circuit Breaker.
O que é Circuit Breaker?
Circuit Breaker, ou disjuntor, é um padrão utilizado para aumentar a resiliência de aplicações que dependem de outros serviços.
A ideia é simples:
Quando uma dependência começa a falhar repetidamente, pare temporariamente de chamá-la.
O nome vem de uma analogia com o disjuntor elétrico.
Quando existe um problema, o disjuntor interrompe o circuito para evitar que uma falha provoque consequências maiores.
No software, a lógica é semelhante:
Serviço externo começa a falhar
↓
Circuit Breaker detecta
↓
Circuito é aberto
↓
Novas chamadas são bloqueadas
↓
Depois de um tempo
↓
Sistema testa novamente
O objetivo não é corrigir o serviço externo.
O objetivo é proteger sua aplicação enquanto a dependência está indisponível ou degradada.
O problema das falhas em cascata
Considere uma aplicação de vendas.
Quando uma venda é concluída, o sistema precisa conversar com uma API fiscal:
Cliente
↓
Ruby on Rails
↓
API Fiscal
Normalmente, a API responde em aproximadamente 200 ms.
Tudo funciona.
Agora imagine que a API começa a apresentar problemas e cada requisição passa a levar 10 segundos.
A aplicação continua enviando chamadas.
Com poucas requisições, talvez ninguém perceba.
Com centenas ou milhares:
Rails
├── requisição → API
├── requisição → API
├── requisição → API
├── requisição → API
├── requisição → API
├── requisição → API
└── ...
As requisições ficam esperando.
Conexões ficam ocupadas.
Filas aumentam.
Recursos do servidor são consumidos.
E o problema começa a se espalhar.
Temos então uma falha em cascata.
Uma dependência externa que estava com problemas começa a prejudicar também a aplicação que dependia dela.
Os três estados do Circuit Breaker
O padrão tradicional trabalha com três estados:
┌───────────┐
│ CLOSED │
└─────┬─────┘
│
muitas falhas
↓
┌───────────┐
│ OPEN │
└─────┬─────┘
│
tempo passa
↓
┌────────────┐
│ HALF-OPEN │
└─────┬──────┘
│
teste funciona
↓
CLOSED
CLOSED
É o estado normal.
As chamadas são realizadas normalmente.
Rails
↓
Circuit Breaker
↓
API externa
Se tudo estiver funcionando, o circuito permanece CLOSED.
OPEN
Agora imagine que a API apresentou várias falhas consecutivas.
Por exemplo:
Falha
Falha
Falha
Falha
Falha
Ao atingir o limite configurado:
CLOSED → OPEN
A partir desse momento, novas chamadas podem ser bloqueadas.
Em vez de continuar chamando uma API que está indisponível, a aplicação pode responder imediatamente ou utilizar um fallback.
HALF-OPEN
O circuito não pode ficar aberto para sempre.
Depois de determinado período, o sistema pode permitir uma chamada de teste:
OPEN
↓
aguarda
↓
HALF-OPEN
↓
teste
Se a API voltou:
HALF-OPEN → CLOSED
Se continua apresentando falhas:
HALF-OPEN → OPEN
Esse pequeno teste é fundamental para permitir a recuperação automática.
Circuit Breaker não é Retry
É muito comum confundir os dois.
Retry significa:
“A chamada falhou. Vou tentar novamente.”
Circuit Breaker significa:
“Esse serviço está falhando demais. Vou parar temporariamente de chamá-lo.”
Eles podem trabalhar juntos.
Por exemplo:
Requisição
↓
Falhou
↓
Retry
↓
Falhou novamente
↓
Retry
↓
Falhou
↓
Circuit Breaker
↓
OPEN
O Retry tenta recuperar uma operação.
O Circuit Breaker evita que o sistema continue insistindo em uma dependência degradada.
E o Timeout?
Também existe uma diferença importante entre Circuit Breaker e timeout.
Timeout responde:
“Quanto tempo estou disposto a esperar?”
Por exemplo:
Timeout = 5 segundos
Se a API não responder:
5 segundos
↓
timeout
↓
requisição encerrada
O Circuit Breaker responde a outra pergunta:
“Depois de tantas falhas, devo continuar chamando esse serviço?”
Em uma aplicação robusta, os mecanismos podem trabalhar juntos:
Timeout
↓
Retry
↓
Circuit Breaker
Cada um tem uma responsabilidade diferente.
Circuit Breaker com Ruby on Rails
Agora vamos para um exemplo mais próximo de uma aplicação real.
Imagine uma aplicação construída com:
- Ruby on Rails;
- PostgreSQL;
- Redis;
- Sidekiq;
- uma API externa de emissão fiscal.
O fluxo pode ser:
PostgreSQL
▲
│
Cliente → Rails ─────────┤
│
▼
Redis
│
▼
Sidekiq
│
▼
Circuit Breaker
│
▼
API Fiscal
Cada componente possui uma responsabilidade.
Rails cuida da aplicação.
PostgreSQL mantém os dados persistentes.
Redis pode armazenar estado temporário e é utilizado pelo Sidekiq.
Sidekiq executa tarefas em background.
Circuit Breaker protege a comunicação com a API externa.
Criando uma venda
Imagine um model simples:
class Sale < ApplicationRecord
enum :status, {
pending: 0,
processing: 1,
authorized: 2,
failed: 3
}
end
No PostgreSQL:
sales
--------------------------------
id
customer_id
total
status
created_at
updated_at
Quando o cliente realiza uma compra:
sale = Sale.create!(
customer_id: 10,
total: 150.00,
status: :pending
)
A venda foi registrada.
Agora precisamos enviá-la para o serviço fiscal.
Em vez de fazer isso diretamente durante a requisição do usuário, podemos utilizar o Sidekiq.
Processando a emissão com Sidekiq
Criamos um worker:
class FiscalInvoiceJob
include Sidekiq::Job
def perform(sale_id)
sale = Sale.find(sale_id)
FiscalApi.new.issue_invoice(sale)
sale.update!(status: :authorized)
end
end
E colocamos o trabalho na fila:
FiscalInvoiceJob.perform_async(sale.id)
Agora temos:
Rails
↓
Redis
↓
Sidekiq
↓
FiscalInvoiceJob
↓
API Fiscal
A requisição do usuário não precisa ficar esperando a API externa.
E se a API fiscal cair?
Imagine:
Venda 1001 → API ❌
Venda 1002 → API ❌
Venda 1003 → API ❌
Venda 1004 → API ❌
Venda 1005 → API ❌
O Sidekiq pode realizar novas tentativas.
Mas existe um problema:
e se existirem milhares de jobs?
Podemos acabar com:
Sidekiq
│
├── Job 1 → API ❌
├── Job 2 → API ❌
├── Job 3 → API ❌
├── Job 4 → API ❌
├── Job 5 → API ❌
└── ...
É nesse momento que o Circuit Breaker passa a ser interessante.
Um Circuit Breaker simples usando Redis
Para entender o conceito, podemos criar uma implementação didática:
class CircuitOpenError < StandardError
end
Agora o Circuit Breaker:
class FiscalCircuitBreaker
KEY = "circuit_breaker:fiscal_api"
FAILURE_LIMIT = 5
OPEN_TIME = 30.seconds
def initialize(redis = Redis.current)
@redis = redis
end
def call
state = @redis.hget(KEY, "state") || "CLOSED"
if state == "OPEN"
opened_at = @redis.hget(KEY, "opened_at").to_f
if Time.current.to_f - opened_at < OPEN_TIME
raise CircuitOpenError, "Circuit Breaker está aberto"
end
@redis.hset(KEY, "state", "HALF_OPEN")
end
begin
result = yield
reset!
result
rescue => error
register_failure!
raise error
end
end
private
def register_failure!
failures = @redis.hincrby(KEY, "failures", 1)
if failures >= FAILURE_LIMIT
@redis.hset(
KEY,
"state", "OPEN",
"opened_at", Time.current.to_f
)
end
end
def reset!
@redis.hset(
KEY,
"state", "CLOSED",
"failures", 0
)
end
end
Esse código é propositalmente simples.
Ele não pretende ser uma implementação pronta para produção.
O objetivo é mostrar como o mecanismo funciona.
Utilizando o Circuit Breaker no Sidekiq
Agora podemos alterar nosso worker:
class FiscalInvoiceJob
include Sidekiq::Job
def perform(sale_id)
sale = Sale.find(sale_id)
FiscalCircuitBreaker.new.call do
FiscalApi.new.issue_invoice(sale)
end
sale.update!(status: :authorized)
end
end
Agora o fluxo é:
Sidekiq
↓
Circuit Breaker
↓
API Fiscal
Se a API estiver funcionando:
CLOSED
↓
API
↓
sucesso
Se começar a falhar:
CLOSED
↓
falhas
↓
OPEN
E novas chamadas podem ser bloqueadas.
Onde o Redis entra?
Nesse exemplo, o Redis guarda o estado temporário do Circuit Breaker:
circuit_breaker:fiscal_api
state = OPEN
failures = 5
opened_at = ...
Isso é útil porque podemos ter vários processos Sidekiq trabalhando simultaneamente.
Mas existe um detalhe importante:
implementar isso corretamente em um ambiente concorrente é mais complexo do que parece.
Precisamos pensar em:
- concorrência;
- locks;
- operações atômicas;
- múltiplos processos;
- múltiplos servidores;
- estado
HALF-OPEN; - TTL;
- recuperação;
- observabilidade.
Por isso, uma implementação real normalmente utiliza uma biblioteca madura ou uma implementação cuidadosamente projetada e testada.
PostgreSQL e Redis têm responsabilidades diferentes
É importante não confundir os papéis.
A venda pertence ao domínio do negócio:
PostgreSQL
↓
Sale
status = pending
O estado do Circuit Breaker é temporário:
Redis
↓
fiscal_api
state = OPEN
Assim:
PostgreSQL mantém o estado persistente da venda.
Redis pode coordenar o estado temporário do mecanismo de proteção.
Essa separação é importante para a arquitetura.
E quando a API voltar?
Depois dos 30 segundos configurados:
OPEN
↓
30 segundos
↓
HALF-OPEN
Uma chamada de teste é permitida.
Se funcionar:
HALF-OPEN
↓
sucesso
↓
CLOSED
Se falhar:
HALF-OPEN
↓
falha
↓
OPEN
O sistema se recupera automaticamente quando a dependência volta a responder.
Um detalhe importante: Sidekiq Retry
Aqui precisamos tomar cuidado.
Sidekiq já possui mecanismo de retry.
Portanto, não devemos simplesmente criar:
raise CircuitOpenError
e assumir que tudo estará resolvido.
Podemos acabar com:
Job
↓
Circuit OPEN
↓
Retry
↓
Circuit OPEN
↓
Retry
↓
Circuit OPEN
Ou seja, o próprio mecanismo de recuperação pode acabar pressionando novamente uma dependência que estamos tentando proteger.
Por isso, Retry e Circuit Breaker precisam ser projetados em conjunto.
A estratégia depende do comportamento desejado para aquele tipo de job.
Timeout também faz parte da proteção
Nossa chamada para a API externa deveria possuir timeout.
Por exemplo, utilizando Faraday:
response = Faraday.post(url, body, headers) do |request|
request.options.timeout = 5
request.options.open_timeout = 2
end
Agora temos diferentes camadas:
Timeout
↓
"Não espere indefinidamente."
Retry
↓
"Tente novamente."
Circuit Breaker
↓
"Pare de chamar temporariamente."
Sidekiq
↓
"Execute o trabalho em background."
Redis
↓
"Coordene estado temporário e filas."
PostgreSQL
↓
"Persista o estado do negócio."
Essa separação de responsabilidades é uma das partes mais importantes da arquitetura.
E o Fallback?
Nem toda falha precisa resultar em erro para o usuário.
Dependendo da regra de negócio, podemos ter um fallback.
Por exemplo:
API de consulta
↓
❌
↓
Circuit Breaker
↓
Cache / fallback
↓
Resposta alternativa
Mas isso precisa ser tratado com cuidado.
Em uma consulta que aceita dados temporariamente armazenados, um fallback pode ser perfeitamente aceitável.
Em uma operação financeira ou fiscal, utilizar uma informação antiga pode ser perigoso.
Portanto:
Fallback é uma decisão de negócio, não simplesmente uma decisão técnica.
Não transforme o Circuit Breaker em uma caixa-preta
Existe uma lição importante aqui, principalmente em uma época em que muitas aplicações estão sendo desenvolvidas com auxílio de IA.
Não basta copiar uma biblioteca e escrever:
CircuitBreaker.call do
...
end
Precisamos saber:
- quais erros abrem o circuito;
- qual é o limite de falhas;
- quanto tempo o circuito permanece aberto;
- como funciona o
HALF-OPEN; - quais chamadas podem ser bloqueadas;
- como o Sidekiq tratará os jobs;
- qual é o fallback;
- como o comportamento será monitorado.
Usar um padrão é diferente de compreender o padrão.
E isso vale para Circuit Breaker, filas, cache, banco de dados, microsserviços e praticamente qualquer outra tecnologia.
Circuit Breaker em uma arquitetura Rails
Podemos resumir nosso exemplo assim:
┌──────────────┐
│ PostgreSQL │
│ │
│ Sale │
└──────▲───────┘
│
│
Cliente ──→ Rails ──────────────┤
│
▼
Redis
│
▼
Sidekiq
│
▼
Circuit Breaker
│
▼
API Fiscal
Quando tudo está funcionando:
Rails
↓
Sidekiq
↓
Circuit Breaker
↓
API Fiscal
↓
sucesso
↓
PostgreSQL
Quando a API apresenta problemas:
Rails
↓
Sidekiq
↓
Circuit Breaker
↓
OPEN
↓
chamada bloqueada
A aplicação deixa de pressionar uma dependência que já demonstrou estar indisponível.
O que aprendemos?
Circuit Breaker não é uma tecnologia específica do Ruby on Rails.
É um padrão arquitetural.
Ele pode ser utilizado em praticamente qualquer stack que dependa de serviços externos.
No nosso exemplo:
| Tecnologia | Responsabilidade |
|---|---|
| Ruby on Rails | Aplicação |
| PostgreSQL | Dados persistentes |
| Redis | Estado temporário e infraestrutura |
| Sidekiq | Processamento assíncrono |
| Timeout | Limite de espera |
| Retry | Novas tentativas |
| Circuit Breaker | Proteção contra dependências degradadas |
| Fallback | Estratégia alternativa |
O mais importante é entender que cada mecanismo resolve um problema diferente.
Conclusão
Sistemas distribuídos falham.
APIs ficam indisponíveis.
Serviços ficam lentos.
Redes apresentam problemas.
Bancos de dados podem ficar temporariamente inacessíveis.
Não existe arquitetura capaz de eliminar completamente essas situações.
Uma boa arquitetura é aquela que espera que as falhas aconteçam e define previamente como o sistema deve reagir a elas.
O Circuit Breaker é uma dessas estratégias.
Ele não corrige a API externa.
Ele não elimina a falha.
Ele não torna o sistema indestrutível.
O que ele faz é muito mais simples:
Quando uma dependência começa a falhar, ele impede que nossa aplicação continue insistindo até que o problema externo se transforme também em um problema interno.
E essa é a essência da resiliência.
Para guardar
Timeout
→ quanto tempo esperar?
Retry
→ tentar novamente?
Circuit Breaker
→ devo parar temporariamente de chamar?
Fallback
→ o que posso fazer enquanto o serviço está indisponível?
Sidekiq
→ posso executar essa operação em background?
Redis
→ como coordenar estado temporário e filas?
PostgreSQL
→ quais dados precisam ser persistidos?
Quando você consegue responder essas perguntas, Circuit Breaker deixa de ser apenas mais um termo de arquitetura e passa a fazer sentido dentro do sistema.
Resiliência não é impedir que sistemas falhem. É projetá-los para falhar de forma controlada, observável e recuperável.