---
title: "Circuit Breaker em Go: Resiliência para APIs Externas"
url: "https://golang.com.br/blog/circuit-breaker-go-http-resiliencia/"
markdown_url: "https://golang.com.br/blog/circuit-breaker-go-http-resiliencia.MD"
description: "Aprenda circuit breaker em Go para chamadas HTTP: estados, timeouts, fallback, half-open, métricas, testes e cuidados de produção."
date: "2026-06-07"
author: "Golang Brasil"
---

# Circuit Breaker em Go: Resiliência para APIs Externas

Aprenda circuit breaker em Go para chamadas HTTP: estados, timeouts, fallback, half-open, métricas, testes e cuidados de produção.


Circuit breaker em Go é um padrão simples para um problema caro: impedir que uma dependência instável derrube o seu serviço por arrasto. Quando uma API de pagamento, antifraude, frete, nota fiscal, CRM ou autenticação começa a falhar, o pior comportamento é continuar abrindo chamadas sem limite, segurando goroutines, ocupando conexões e aumentando a latência de todos os usuários. O breaker corta o caminho antes que a falha vire efeito dominó.

Em português direto: circuit breaker é um disjuntor lógico. Enquanto a dependência responde bem, as chamadas passam. Quando os erros passam de um limite, o circuito abre e novas chamadas falham rápido. Depois de um tempo, ele deixa poucas tentativas passarem no modo half-open. Se a dependência se recuperou, fecha de novo. Se continua ruim, mantém o corte.

Este guia mostra como aplicar circuit breaker em Go para clients HTTP, com `context.Context`, timeouts, fallback, métricas, logs, testes e cuidados de produção. Ele complementa [context, timeout e cancelamento](/blog/context-timeout-cancelamento-go/), [httptrace para debug de cliente HTTP](/blog/httptrace-go-debug-http-client/), [rate limiting em Go](/blog/rate-limiting-go-api-producao/), [OpenTelemetry em Go](/blog/go-opentelemetry-observabilidade-tracing-metricas/) e [webhooks com idempotência](/blog/webhooks-go-assinatura-idempotencia/).

## Quando usar circuit breaker

Use circuit breaker quando a sua aplicação depende de algo que pode ficar lento, instável ou indisponível por alguns minutos: API externa, gateway de pagamento, serviço interno, banco secundário, cache remoto ou fornecedor B2B. O objetivo não é esconder erro. É proteger capacidade, degradar com clareza e dar tempo para a dependência se recuperar.

Bons sinais de que o padrão faz sentido:

- chamadas HTTP externas aparecem no caminho crítico do request;
- timeouts de fornecedor já causaram picos de latência;
- retries aumentam a carga em vez de resolver;
- dashboards mostram cascata de erro entre serviços;
- usuários poderiam receber uma resposta parcial, fila ou mensagem de indisponibilidade controlada;
- o time precisa de métrica clara de dependência aberta, fechada e em recuperação.

Não use breaker para mascarar bug determinístico do seu próprio código. Se toda chamada falha por payload inválido, credencial errada ou contrato quebrado, o conserto é validação e deploy, não disjuntor. Também não use como substituto para timeout. Uma chamada sem timeout pode prender o worker antes mesmo de o breaker contabilizar erro.

## Estados: closed, open e half-open

O breaker opera em três estados:

1. **Closed**: funcionamento normal. As chamadas passam e o breaker contabiliza sucesso, erro e timeout.
2. **Open**: a taxa ou sequência de falhas passou do limite. O breaker falha rápido sem chamar a dependência.
3. **Half-open**: depois de uma janela de espera, poucas chamadas de teste são permitidas. Se der certo, fecha. Se falhar, abre novamente.

Esse desenho é melhor que retry infinito. Retry pode ser útil para erro transitório, mas aumenta tráfego justamente quando o fornecedor está sob pressão. Circuit breaker reduz o volume, protege seus recursos e deixa o incidente mais previsível.

## Um client HTTP com breaker

A forma mais comum em Go é encapsular o client externo em um tipo próprio. Assim, handler, worker e job não precisam saber se a proteção vem de biblioteca, métrica ou regra local. O exemplo abaixo usa `github.com/sony/gobreaker`, uma biblioteca pequena e conhecida no ecossistema Go.

```bash
go get github.com/sony/gobreaker
```

```go
package antifraude

import (
    "bytes"
    "context"
    "encoding/json"
    "fmt"
    "net/http"
    "time"

    "github.com/sony/gobreaker"
)

type Client struct {
    baseURL string
    http    *http.Client
    breaker *gobreaker.CircuitBreaker
}

type Request struct {
    OrderID string `json:"order_id"`
    Amount  int64  `json:"amount"`
}

type Response struct {
    Approved bool   `json:"approved"`
    Reason   string `json:"reason"`
}

func New(baseURL string) *Client {
    st := gobreaker.Settings{
        Name:        "antifraude-http",
        MaxRequests: 3,
        Interval:    1 * time.Minute,
        Timeout:     30 * time.Second,
        ReadyToTrip: func(counts gobreaker.Counts) bool {
            failureRatio := float64(counts.TotalFailures) / float64(counts.Requests)
            return counts.Requests >= 10 && failureRatio >= 0.50
        },
        OnStateChange: func(name string, from gobreaker.State, to gobreaker.State) {
            // Troque por slog, métrica ou evento de observabilidade.
            fmt.Printf("breaker %s mudou de %s para %s\n", name, from, to)
        },
    }

    return &Client{
        baseURL: baseURL,
        http: &http.Client{
            Timeout: 5 * time.Second,
        },
        breaker: gobreaker.NewCircuitBreaker(st),
    }
}

func (c *Client) Analyze(ctx context.Context, req Request) (Response, error) {
    result, err := c.breaker.Execute(func() (any, error) {
        return c.call(ctx, req)
    })
    if err != nil {
        return Response{}, err
    }
    return result.(Response), nil
}

func (c *Client) call(ctx context.Context, payload Request) (Response, error) {
    body, err := json.Marshal(payload)
    if err != nil {
        return Response{}, err
    }

    httpReq, err := http.NewRequestWithContext(
        ctx,
        http.MethodPost,
        c.baseURL+"/v1/analyze",
        bytes.NewReader(body),
    )
    if err != nil {
        return Response{}, err
    }
    httpReq.Header.Set("Content-Type", "application/json")

    resp, err := c.http.Do(httpReq)
    if err != nil {
        return Response{}, err
    }
    defer resp.Body.Close()

    if resp.StatusCode >= 500 || resp.StatusCode == http.StatusTooManyRequests {
        return Response{}, fmt.Errorf("antifraude indisponível: status %d", resp.StatusCode)
    }
    if resp.StatusCode >= 400 {
        return Response{}, fmt.Errorf("requisição inválida para antifraude: status %d", resp.StatusCode)
    }

    var out Response
    if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
        return Response{}, err
    }
    return out, nil
}
```

Repare em três decisões importantes. Primeiro, o `http.Client` tem timeout. Segundo, a chamada recebe `ctx` de fora, então cancelamento do request, worker ou job continua valendo. Terceiro, nem todo status HTTP deve contar igual. Erros 500 e 429 indicam indisponibilidade ou sobrecarga da dependência. Erros 400 geralmente indicam problema no seu payload; eles devem ir para log e correção, mas não necessariamente abrir o circuito.

## Fallback sem mentir para o usuário

Quando o breaker abre, você precisa decidir como degradar. Fallback bom é honesto. Ele não inventa aprovação de pagamento, não ignora regra de segurança e não marca uma operação crítica como concluída sem evidência. Em alguns sistemas, o fallback correto é negar temporariamente. Em outros, é aceitar a entrada, colocar em fila e informar que a análise será concluída depois.

```go
func Handler(client *antifraude.Client, queue Queue) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second)
        defer cancel()

        result, err := client.Analyze(ctx, antifraude.Request{
            OrderID: "pedido-123",
            Amount:  12990,
        })
        if err != nil {
            if enqueueErr := queue.EnqueueReview(r.Context(), "pedido-123"); enqueueErr != nil {
                http.Error(w, "serviço temporariamente indisponível", http.StatusServiceUnavailable)
                return
            }
            w.WriteHeader(http.StatusAccepted)
            _, _ = w.Write([]byte("pedido recebido para análise assíncrona"))
            return
        }

        if !result.Approved {
            http.Error(w, "pedido em revisão", http.StatusAccepted)
            return
        }

        w.WriteHeader(http.StatusOK)
        _, _ = w.Write([]byte("pedido aprovado"))
    }
}
```

Esse exemplo transforma uma falha externa em processamento assíncrono. Para isso funcionar de verdade, a fila precisa ser idempotente, observável e segura contra duplicidade. Se você ainda não tem esse desenho, leia [idempotência, retry e DLQ em Go](/blog/idempotencia-retry-dlq-go/) antes de colocar fallback em produção.

## Configuração que não vira chute

Os parâmetros do breaker devem refletir o comportamento normal da dependência. Um serviço que recebe dez chamadas por minuto não deve abrir com a mesma regra de um serviço que recebe dez mil chamadas por minuto. Comece conservador:

- `http.Client.Timeout`: menor que o orçamento do request;
- `ReadyToTrip`: exige volume mínimo antes de calcular taxa de falha;
- `Timeout` do breaker: tempo suficiente para a dependência respirar;
- `MaxRequests`: poucas tentativas no half-open para evitar enxurrada;
- `Interval`: janela para limpar contadores quando o sistema está saudável.

Uma regra prática: se o endpoint é crítico e a dependência é instável, prefira falhar rápido com mensagem clara a segurar goroutine até estourar timeout. Se o endpoint é assíncrono, empurre para fila e responda `202 Accepted`. Se é uma consulta opcional, mostre conteúdo principal sem o bloco dependente.

## Métricas e logs obrigatórios

Circuit breaker sem observabilidade vira superstição. Você precisa saber quando ele abriu, por quê, por quanto tempo e qual foi o impacto no usuário. No mínimo, registre:

- estado atual do breaker por dependência;
- mudanças de estado com `from` e `to`;
- total de chamadas permitidas, falhas rápidas e chamadas reais;
- status HTTP e tipo de erro da dependência;
- latência antes do erro;
- número de fallbacks executados;
- volume de operações mandadas para fila.

Com [slog em Go](/blog/slog-go-logging-estruturado/), um evento de mudança de estado pode carregar campos estáveis:

```go
logger.Warn("circuit breaker mudou de estado",
    "name", name,
    "from", from.String(),
    "to", to.String(),
)
```

Com [OpenTelemetry em Go](/blog/go-opentelemetry-observabilidade-tracing-metricas/), adicione atributos como `dependency.name`, `breaker.state` e `fallback.used`. O ponto não é criar dashboard bonito. É responder rápido se o problema está no seu serviço, no fornecedor, na rede ou no excesso de retry.

## Testando o comportamento

Teste de circuit breaker não deve depender de dormir trinta segundos em tempo real. Use configurações curtas no teste e um servidor `httptest` controlado. O objetivo é provar três coisas: abre depois de falhas suficientes, falha rápido quando está aberto e volta a permitir chamadas depois da janela.

```go
func TestBreakerAbreDepoisDeFalhas(t *testing.T) {
    calls := 0
    srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        calls++
        http.Error(w, "down", http.StatusInternalServerError)
    }))
    defer srv.Close()

    c := New(srv.URL)

    for i := 0; i < 12; i++ {
        _, _ = c.Analyze(context.Background(), Request{OrderID: "x", Amount: 100})
    }

    before := calls
    _, err := c.Analyze(context.Background(), Request{OrderID: "x", Amount: 100})
    if err == nil {
        t.Fatal("esperava erro com breaker aberto")
    }
    if calls != before {
        t.Fatal("breaker aberto não deveria chamar o servidor")
    }
}
```

Em código real, ajuste o construtor para aceitar `gobreaker.Settings` no teste. Isso evita acoplar teste ao tempo de produção e permite simular janelas curtas. Também vale testar quais erros contam como falha. Um `400 Bad Request` por payload inválido não deve ter o mesmo peso de `503 Service Unavailable`.

## Erros comuns em produção

O primeiro erro é combinar retry agressivo com breaker frouxo. Se cada requisição tenta três vezes e o breaker só abre depois de dezenas de falhas, você multiplica a carga antes de proteger o sistema. Retry precisa de backoff, jitter e limite. Em muitos fluxos de request síncrono, zero ou um retry já é suficiente.

O segundo é compartilhar um breaker único para dependências diferentes. API de pagamento, CEP, CRM e e-mail têm perfis de falha distintos. Um fornecedor ruim não deve abrir circuito de outro. Use um breaker por dependência e, em alguns casos, por operação crítica.

O terceiro é esquecer que cache também falha. Se Redis é otimização, falha de cache pode virar miss e seguir para banco. Se Redis é parte do contrato, como sessão ou lock, a degradação precisa ser mais cuidadosa. O tutorial de [Go e Redis para cache](/tutoriais/go-redis-cache/) ajuda a separar cache opcional de dependência crítica.

O quarto é esconder erro demais. Se o fallback sempre retorna uma resposta aparentemente normal, produto, suporte e SRE perdem o sinal. Prefira uma resposta parcial explícita, um `202 Accepted`, uma mensagem de indisponibilidade temporária ou um status que permita ação do usuário.

O quinto é não conectar breaker a deploy. Durante incidentes, você pode precisar reduzir tráfego, desligar integração, trocar fornecedor ou ativar feature flag. Combine o padrão com [feature flags em Go](/blog/feature-flags-go-rollout-seguro/) para ter controle operacional sem publicar código às pressas.

## Checklist para APIs resilientes em Go

Antes de colocar um client protegido em produção, confira:

- toda chamada externa tem `context.Context` e timeout explícito;
- status 5xx e 429 contam como falha de dependência;
- erros 4xx são tratados separadamente quando indicam bug de payload;
- breaker é separado por fornecedor/operação importante;
- fallback não viola segurança, pagamento ou privacidade;
- métricas mostram estado, falhas rápidas e uso de fallback;
- logs incluem dependência, operação, status e correlação de request;
- testes cobrem abertura, falha rápida e recuperação;
- retry, backoff e breaker não se multiplicam sem limite;
- runbook explica o que fazer quando o circuito abre.

Circuit breaker não deixa uma arquitetura automaticamente resiliente. Ele só torna a falha explícita, limitada e mensurável. A maturidade vem de combinar timeout, limitação de concorrência, idempotência, fila, observabilidade e mensagens honestas para o usuário.

Se você está se preparando para vagas Go backend, esse é um ótimo tema para entrevista porque conecta código, infraestrutura e operação. Muitas [vagas Go no Brasil](/vagas/) pedem APIs, microserviços, cloud, observabilidade e sistemas distribuídos. Para comparar como resiliência aparece em outras stacks brasileiras, acompanhe também o portal <a href="https://eu.dev.br/vagas/" target="_blank" rel="noopener noreferrer" onclick="umami.track('portfolio-site-click', { destination: 'eu.dev.br' })">eu.dev.br reúne vagas de tecnologia no Brasil</a>, onde padrões como timeout, retry, fila e observabilidade aparecem independentemente da linguagem.
