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.