Patrón Circuit Breaker en Go: Detener las Fallos en Cascada

Evita las falencias en cascada en microservicios de Go.

Índice

Un breaker de circuitos (o disyuntor eléctrico) impide que tu servicio Go siga presionando una dependencia fallida, previniendo fallos en cascada que consumen gorutinas, sockets y memoria hasta que todo el sistema colapse.

Lo difícil no es la máquina de estados. Es decidir dónde debe ubicarse el breaker, qué cuenta como fallo, cómo interactúa con los timeouts y reintentos, y qué debe hacer tu servicio cuando el circuito está abierto.

breaker de circuitos

En Go, el patrón de breaker de circuitos es especialmente útil alrededor de las llamadas salientes: APIs HTTP, pasarelas de pago, servicios de búsqueda, proveedores de correo electrónico, gateways de LLM, microservicios internos y otras dependencias que pueden volverse lentas, sobrecargadas o parcialmente inaccessibles. Usado correctamente, un breaker de circuitos reduce los fallos en cascada. Usado incorrectamente, se convierte en otro modo de fallo oscuro.

¿Qué problema resuelve un Breaker de Circuitos?

Los sistemas distribuidos rara vez fallan de forma limpia.

Una dependencia podría no estar completamente caída. Podría estar:

  • devolviendo errores 500
  • devolviendo respuestas de límite de tasa 429
  • aceptando conexiones TCP pero sin responder nunca
  • respondiendo en 30 segundos en lugar de 300 milisegundos
  • fallando solo para algunas solicitudes
  • sobrecargada porque cada cliente está reintentando al mismo tiempo

El peor caso a menudo no es un fallo duro. Es una dependencia lenta.

Las llamadas lentas consumen gorutinas, sockets, conexiones de base de datos, memoria y capacidad de trabajo. Si tu servicio sigue esperando a una dependencia que ya está enferma, tu propio servicio también puede volverse enfermo.

Un breaker de circuitos previene eso fallando rápidamente después de que la dependencia cruce un umbral de fallo.

En lugar de hacer esto para siempre:

solicitud -> llamar dependencia -> esperar -> timeout -> reintentar -> esperar -> fallar

el servicio eventualmente hace esto:

solicitud -> circuito abierto -> devolver fallback o error inmediatamente

Ese fallo rápido no siempre es agradable, pero es predecible. Un fallo predecible es más fácil de operar que un colapso lento.

Los tres estados del Breaker de Circuitos

La mayoría de los breakers de circuitos utilizan tres estados.

Cerrado (Closed)

El circuito está cerrado durante el funcionamiento normal.

Las solicitudes son permitidas. El breaker registra éxitos y fallos. Si el número o la proporción de fallos cruza un umbral, el breaker se abre.

Cerrado no significa “seguro para siempre”. Significa “el tráfico está permitido actualmente”.

Abierto (Open)

El circuito está abierto cuando la dependencia se considera enferma.

Las solicitudes son rechazadas inmediatamente. El servicio debe devolver una respuesta de respaldo (fallback), una respuesta en caché, una respuesta degradada o un error claro del upstream.

Abierto no repara la dependencia. Le da tiempo a la dependencia para recuperarse y protege al cliente de desperdiciar recursos.

Semiabierto (Half-Open)

Después de un período de enfriamiento, el breaker entra en un estado semiabierto.

Solo se permiten un número limitado de solicitudes de prueba. Si tienen éxito, el breaker se cierra. Si fallan, el breaker se vuelve a abrir.

El estado semiabierto es importante porque evita dos extremos malos:

  • nunca intentar la dependencia de nuevo
  • enviar todo el tráfico de vuelta demasiado rápido

Las transiciones de estado se ven así:

stateDiagram-v2 [*] --> Closed Closed --> Open: Umbral de fallo alcanzado Open --> HalfOpen: Timeout transcurrido HalfOpen --> Closed: Prueba exitosa HalfOpen --> Open: Prueba fallida

Breaker de Circuitos vs Timeout vs Reintento

Un error común es tratar los breakers de circuitos, los reintentos y los timeouts como intercambiables. Están relacionados, pero resuelven problemas diferentes.

Timeout

Un timeout limita cuánto tiempo puede durar una operación.

En Go, esto usualmente significa pasar un context.Context con un deadline o timeout a la llamada saliente.

Un timeout responde a esta pregunta:

¿Cuánto tiempo estoy dispuesto a esperar por esta llamada?

Reintento (Retry)

Un reintito repite una operación cuando el fallo podría ser temporal.

Los reintentos son útiles para pequeños fallos de red, respuestas 503 temporales, restablecimientos de conexión y otros fallos transitorios.

Un reintento responde a esta pregunta:

¿Debería intentar esta llamada de nuevo?

Breaker de Circuitos

Un breaker de circuitos detiene las llamadas cuando la dependencia probablemente está enferma.

Responde a esta pregunta:

¿Debería llamar a esta dependencia en absoluto en este momento?

Limitador de Tasa (Rate Limiter)

Un limitador de tasa controla cuánt tráfico se permite a lo largo del tiempo.

Responde a esta pregunta:

¿Cuánto tráfico debería enviar este cliente?

Muro de Contención (Bulkhead)

Un bulkhead aísla los recursos para que una dependencia no pueda consumir todo.

Responde a esta pregunta:

¿Cuánto de mi servicio puede dañar esta dependencia?

Estos patrones son más fuertes cuando se usan juntos. Un breaker de circuitos sin timeouts es débil. Los reintentos sin jitter pueden crear tormentas de reintento. Un fallback sin métricas puede ocultar una interrupción del servicio.

Cuándo usar un Breaker de Circuitos en Go

Usa un breaker de circuitos cuando tu servicio llama a una dependencia que puede fallar independientemente de tu servicio.

Buenos candidatos incluyen:

  • APIs HTTP externas
  • Procesadores de pago
  • Proveedores de correo electrónico y SMS
  • Servicios de búsqueda
  • Servicios de recomendación
  • Gateways de inferencia de LLM
  • Puntos finales de microservicios internos
  • APIs de SaaS de terceros
  • Servicios de lectura lentos o sobrecargados

Los breakers de circuitos son especialmente útiles cuando el cliente puede degradarse elegantemente.

Por ejemplo:

  • devolver datos de productos en caché
  • omitir un bloque de recomendaciones
  • marcar un proveedor de pagos como temporalmente no disponible
  • poner el trabajo en cola para más tarde, con una cola de mensajes fallidos (dead-letter queue) como respaldo para los mensajes que nunca tienen éxito incluso después de que la dependencia se recupera
  • devolver una respuesta parcial
  • fallar rápido con un error temporal claro

La pregunta importante no es “¿puede fallar esta llamada?”. Todo puede fallar. La mejor pregunta es:

Si esta dependencia está fallando, ¿debemos seguir enviando todo el tráfico hacia ella?

Si la respuesta es no, un breaker de circuitos puede ayudar.

Cuándo NO usar un Breaker de Circuitos

No añadas un breaker de circuitos a cada función solo porque el patrón suena responsable.

Un breaker de circuitos usualmente no es útil para:

  • llamadas a funciones locales dentro del mismo proceso
  • CRUD simple dentro de un monolito
  • lógica de validación
  • reglas de negocio deterministas
  • operaciones locales solo de CPU
  • rutas de código donde no existe un fallback útil
  • operaciones de escritura que no son idempotentes
  • dependencias ya protegidas por una capa de flujo de trabajo más robusta

Un breaker de circuitos tampoco reemplaza la higiene básica:

  • establece timeouts
  • propaga contextos
  • usa pools de conexión correctamente
  • maneja errores explícitamente
  • haz que los reintentos sean seguros
  • observa las tasas de fallo

Un mal breaker de circuitos puede hacer que un sistema sea más difícil de razonar. Puede ocultar el problema real, rechazar tráfico demasiado agresivo o crear comportamientos confusos durante la recuperación.

La regla ligeramente opinativa es simple:

Añade breakers de circuitos en los límites de las dependencias, no en todas partes.

Eligiendo una librería de Breaker de Circuitos en Go

Puedes implementar un breaker de circuitos básico tú mismo, pero la mayoría de los servicios Go en producción deberían usar una librería.

La opción simple más común es sony/gobreaker.

Te ofrece:

  • estados cerrado, abierto y semiabierto
  • umbrales de fallo configurables
  • timeout configurable para el estado abierto
  • callbacks de cambio de estado
  • contadores de solicitudes
  • soporte genérico en la versión 2
  • una superficie de API pequeña

Para pipelines de resiliencia más grandes, también puedes buscar librerías que componen múltiples políticas, como reintento, timeout, fallback, limitación de tasa, aislamiento bulkhead y breaker de circuitos. Esto puede ser útil cuando quieres una sola capa de resiliencia alrededor de una operación.

Para muchos servicios Go, sin embargo, gobreaker es suficiente.

Comparación de Paquetes de Breaker de Circuitos en Go

Go no incluye un breaker de circuitos integrado en la biblioteca estándar. En la práctica, usualmente eliges entre una librería pequeña de breaker de circuitos, un framework de resiliencia más grande o un paquete más antiguo estilo Hystrix.

Para la mayoría de los nuevos servicios Go, la decisión es simple:

  • usa sony/gobreaker si quieres un breaker de circuitos pequeño y enfocado
  • usa failsafe-go si quieres breakers de circuitos compuestos con reintentos, timeouts, fallbacks, bulkheads, límites de tasa y otras políticas de resiliencia
  • evita comenzar nuevos proyectos con hystrix-go a menos que ya tengas código legado usándolo
Paquete Mejor para Fortalezas Compromisos
sony/gobreaker/v2 Breakers simples alrededor de clientes HTTP/RPC API pequeña, soporte genérico v2, modelo de estados claro, fácil de envolver clientes de dependencia Solo resuelve el breaker de circuitos; los reintentos, timeouts y fallbacks deben componerse por separado
failsafe-go Composición completa de políticas de resiliencia Reintento, fallback, breaker de circuitos, timeout, bulkhead, limitador de tasa, caché, hedge, limitador adaptativo y throttler adaptativo Más conceptos para aprender; más pesado de lo necesario si solo quieres un breaker básico
afex/hystrix-go Sistemas estilo Hystrix heredados Conceptos familiares de Hystrix, ejecución estilo comando, uso histórico Diseño antiguo; no es la mejor opción por defecto para nuevos servicios Go
go-kit/kit/circuitbreaker Servicios basados en endpoints de Go kit Se ajusta al estilo middleware de Go kit y arquitectura de endpoints Mayormente útil si tu servicio ya usa Go kit
cep21/circuit Comportamiento de breaker estilo Hystrix Enfoque más completo estilo Hystrix Menos común como la opción simple por defecto; puede ser más de lo necesario para servicios pequeños

Mi recomendación por defecto es aburrida a propósito: comienza con sony/gobreaker/v2 cuando solo necesitas un breaker de circuitos. Recurre a failsafe-go cuando quieres expresar una política completa de resiliencia en un solo lugar.

Esa separación mantiene la arquitectura limpia. Un pequeño cliente de servicio no necesita un framework completo de resiliencia solo para dejar de llamar a una dependencia fallida. Pero una pasarela, agregador, SDK de cliente API o capa de integración de alto tráfico puede beneficiarse de políticas compuestas.

Instalando gobreaker

Usa el paquete v2 para nuevo código:

go get github.com/sony/gobreaker/v2

Luego impórtalo:

import "github.com/sony/gobreaker/v2"

Un Breaker de Circuitos Básico en Go

Aquí hay un pequeño ejemplo alrededor de una llamada HTTP.

package main

import (
    "context"
    "errors"
    "fmt"
    "io"
    "net/http"
    "time"

    "github.com/sony/gobreaker/v2"
)

var ErrTemporaryUnavailable = errors.New("dependencia temporalmente no disponible")

type UserClient struct {
    baseURL string
    http    *http.Client
    cb      *gobreaker.CircuitBreaker[[]byte]
}

func NewUserClient(baseURL string) *UserClient {
    settings := gobreaker.Settings{
        Name:        "user-service",
        MaxRequests: 3,
        Interval:    30 * time.Second,
        Timeout:     10 * time.Second,
        ReadyToTrip: func(counts gobreaker.Counts) bool {
            return counts.ConsecutiveFailures >= 5
        },
        OnStateChange: func(name string, from gobreaker.State, to gobreaker.State) {
            fmt.Printf("circuit breaker %s changed from %s to %s\n", name, from, to)
        },
    }

    return &UserClient{
        baseURL: baseURL,
        http: &http.Client{
            Timeout: 3 * time.Second,
        },
        cb: gobreaker.NewCircuitBreaker[[]byte](settings),
    }
}

func (c *UserClient) GetUser(ctx context.Context, userID string) ([]byte, error) {
    result, err := c.cb.Execute(func() ([]byte, error) {
        req, err := http.NewRequestWithContext(
            ctx,
            http.MethodGet,
            c.baseURL+"/users/"+userID,
            nil,
        )
        if err != nil {
            return nil, err
        }

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

        if resp.StatusCode >= 500 {
            return nil, fmt.Errorf("user service returned %d", resp.StatusCode)
        }

        if resp.StatusCode == http.StatusNotFound {
            return nil, fmt.Errorf("user not found")
        }

        if resp.StatusCode >= 400 {
            return nil, fmt.Errorf("user service client error: %d", resp.StatusCode)
        }

        return io.ReadAll(resp.Body)
    })

    if errors.Is(err, gobreaker.ErrOpenState) {
        return nil, ErrTemporaryUnavailable
    }

    if errors.Is(err, gobreaker.ErrTooManyRequests) {
        return nil, ErrTemporaryUnavailable
    }

    return result, err
}

Este no es un cliente de producción completo, pero muestra la estructura:

  • el breaker envuelve la llamada saliente
  • la solicitud HTTP recibe un contexto
  • el cliente HTTP tiene un timeout
  • los fallos del lado del servidor cuentan como fallos del breaker
  • los errores de circuito abierto se mapean a un error de aplicación

Configurando los Ajustes de gobreaker

Los ajustes clave valen la pena entenderlos.

Name (Nombre)

Name identifica al breaker.

Usa un nombre estable y específico:

payment-api
search-service
llm-gateway
user-service

Evita nombres vagos como:

http-client
external-call
default

Querrás este nombre en los registros y métricas.

MaxRequests

MaxRequests controla cuántas solicitudes se permiten mientras el breaker está semiabierto.

Un número pequeño suele ser más seguro. El propósito del estado semiabierto es probar la recuperación, no enviar todo el tráfico inmediatamente.

Interval

Interval controla cuándo se borran los contadores internos mientras el breaker está cerrado.

Si es cero, los contadores no se borran automáticamente. Un intervalo no nulo le da al breaker una ventana de memoria tipo “rolling”, aunque no es lo mismo que una implementación completa de ventana deslizante.

Timeout

Timeout controla cuánto tiempo permanece el breaker abierto antes de pasar a semiabierto.

Si el timeout es demasiado corto, tu servicio seguirá sondeando una dependencia que no se ha recuperado. Si es demasiado largo, la recuperación se retrasará.

Comienza con algo conservador, como 10 a 30 segundos, y luego ajusta basándote en las métricas de producción.

ReadyToTrip

ReadyToTrip decide cuándo el breaker debe abrirse.

Una regla simple es fallos consecutivos:

ReadyToTrip: func(counts gobreaker.Counts) bool {
    return counts.ConsecutiveFailures >= 5
}

Eso es fácil de razonar, pero puede no ser lo correcto para servicios de alto volumen.

Otra opción es la proporción de fallos después de un número mínimo de solicitudes:

ReadyToTrip: func(counts gobreaker.Counts) bool {
    total := counts.Requests
    failures := counts.TotalFailures

    if total < 20 {
        return false
    }

    return float64(failures)/float64(total) >= 0.5
}

Esto evita abrir el circuito después de un tamaño de muestra minúsculo.

OnStateChange

OnStateChange es donde deberías emitir registros o métricas.

Como mínimo, registra:

  • nombre del breaker
  • estado antiguo
  • nuevo estado
  • marca de tiempo

Para sistemas de producción, expone el estado del breaker como una métrica. Los registros son útiles para depurar, pero las métricas son mejores para alertas y paneles de control.

IsSuccessful

IsSuccessful te permite decidir qué errores cuentan como fallos.

Esto es importante.

No todos los errores deberían abrir el breaker. Por ejemplo, un 404 Not Found de un servicio de usuario puede ser un resultado empresarial válido. Un 400 Bad Request podría ser culpa del cliente, no de la dependencia.

Un 503 Service Unavailable, timeout, restablecimiento de conexión o 429 Too Many Requests pueden ser una señal real de salud de la dependencia.

Ten cuidado aquí. Contar los errores incorrectos es una de las formas más fáciles de construir un breaker de circuitos ruidoso.

¿Qué debe contarse como Fallo?

Aquí es donde importa el juicio de ingeniería.

Usualmente cuenta estos como fallos:

  • timeouts de red
  • conexión rechazada
  • restablecimiento de conexión
  • HTTP 500
  • HTTP 502
  • HTTP 503
  • HTTP 504
  • respuestas 429 repetidas
  • respuestas malformadas de la dependencia
  • deadline del contexto excedido durante la llamada saliente

Usualmente NO cuentes estos como fallos de dependencia:

  • errores de validación
  • errores de serialización local
  • respuestas 404 esperadas
  • fallos de autorización del lado del cliente
  • rechazos de reglas de negocio
  • errores de entrada del usuario

El breaker debe representar la salud de la dependencia, no el fallo general de la aplicación.

Breakers de Circuitos y context.Context

En Go, los breakers de circuitos no deben reemplazar context.Context.

Un breaker de circuitos decide si intentar una llamada. Un contexto controla cuánto tiempo puede durar esa llamada y si debe detenerse cuando el cliente ha desaparecido.

Una buena llamada saliente usualmente debe tener ambas cosas:

ctx, cancel := context.WithTimeout(parentCtx, 2*time.Second)
defer cancel()

data, err := client.GetUser(ctx, userID)

El contexto debe fluir a través de la cadena de llamadas:

contexto de solicitud entrante
-> método de servicio
-> método del cliente
-> solicitud HTTP
-> dependencia

Evita crear contextos de fondo desconectados dentro del código basado en solicitudes. Si la solicitud del usuario se cancela, el trabajo downstream usualmente también debería detenerse.

La regla tranquila es:

El breaker protege al sistema. El contexto protege a la solicitud.

Normalmente necesitas ambos.

Breakers de Circuitos y Reintentos

Los reintentos y los breakers de circuitos pueden funcionar bien juntos, pero el orden importa.

El predeterminado más seguro es:

timeout por intento
reintento con backoff y jitter
breaker de circuitos alrededor de la llamada a la dependencia

Pero no hay una respuesta universal. Piensa en lo que quieres contar.

Si cada intento de reintento pasa a través del breaker, una solicitud de usuario puede contribuir con múltiples fallos. Eso puede abrir el breaker más rápido, lo cual puede ser bueno o malo.

Si el breaker envuelve toda la operación de reintento, el breaker ve un éxito o fallo final por solicitud de usuario. Eso es más tranquilo, pero puede ocultar el número de intentos fallidos.

Para muchos servicios de aplicación, esta forma es razonable:

solicitud de usuario
-> breaker de circuitos
   -> política de reintento
      -> un intento HTTP con timeout

Eso significa que el breaker rastrea si la operación de dependencia funcionó finalmente para el cliente.

Para clientes de nivel inferior, esta forma también puede tener sentido:

solicitud de usuario
-> política de reintento
   -> breaker de circuitos
      -> un intento HTTP con timeout

Eso significa que el breaker protege cada intento.

La regla más importante es esta:

No reintentes a ciegas.

Usa:

  • un número máximo pequeño de reintentos
  • backoff exponencial
  • jitter
  • timeouts por intento
  • un deadline general de solicitud
  • idempotencia para escrituras
  • métricas para intentos de reintento

Sin eso, los reintentos pueden convertir una pequeña interrupción en una más grande. Para un tratamiento más profundo de la seguridad de los reintentos, consulta Idempotencia en Sistemas Distribuidos que Realmente Funcionan.

Breakers de Circuitos e Idempotencia

Los breakers de circuitos a menudo aparecen junto a los reintentos, y los reintentos plantean la pregunta de la idempotencia.

Para operaciones de lectura, reintentar usualmente es seguro.

Para operaciones de escritura, reintentar puede ser peligroso.

Considera esta llamada de pago:

POST /charge

Si la solicitud tiene un timeout, ¿falló el pago? Quizás. ¿Tuvo éxito pero se perdió la respuesta? También quizás.

Si reintentas sin una clave de idempotencia, podrías cobrar dos veces.

Para operaciones de escritura, usa uno o más de estos:

  • claves de idempotencia
  • IDs de solicitud
  • IDs de operación
  • restricciones únicas
  • caja de salida transaccional (transactional outbox)
  • orquestación de flujos de trabajo
  • reconciliación explícita

Un breaker de circuitos puede detenerte de seguir llamando a un proveedor de pagos fallido, pero no puede hacer seguros los reintentos inseguros.

Breakers de Circuitos y Fallbacks

Cuando el circuito está abierto, tu servicio necesita un plan.

Las posibles estrategias de fallback incluyen:

  • devolver datos en caché
  • devolver datos obsoletos con una advertencia
  • omitir una sección no crítica
  • poner el trabajo en cola para más tarde
  • cambiar a otro proveedor
  • devolver un error temporal
  • mostrar funcionalidad degradada
  • fallar la solicitud rápidamente

Un fallback debe ser honesto.

Por ejemplo, esto usualmente es bueno:

{
  "status": "temporary_unavailable",
  "message": "Las recomendaciones están temporalmente no disponibles"
}

Esto es arriesgado:

{
  "recommendations": []
}

Una lista vacía puede parecer un resultado válido. Puede ocultar una interrupción del servicio, confundir a los usuarios y dificultar la depuración.

Los fallbacks silenciosos son tentadores. También son peligrosos.

Breakers de Circuitos y Observabilidad

Un breaker de circuitos sin observabilidad es mayormente un generador de sorpresas.

Rastrea al menos estas métricas:

  • estado actual del breaker
  • cambios de estado
  • llamadas permitidas
  • llamadas rechazadas
  • éxitos
  • fallos
  • timeouts
  • respuestas de fallback
  • intentos de reintento
  • latencia downstream
  • códigos de estado downstream

Etiquetas útiles incluyen:

  • nombre del breaker
  • nombre de la dependencia
  • nombre de la operación
  • clase de estado
  • categoría de error

Evita etiquetas de alta cardinalidad como ID de usuario, URL completa, ID de solicitud o mensajes de error en bruto.

Deberías poder responder estas preguntas desde los paneles de control:

  • ¿Qué breakers de circuitos están abiertos ahora mismo?
  • ¿Con qué frecuencia se abren?
  • ¿Qué dependencia causó la apertura?
  • ¿Están los usuarios viendo respuestas de fallback?
  • ¿Mejoró la latencia después de que se abrió el breaker?
  • ¿Se disparó el volumen de reintentos antes de que se abriera el breaker?
  • ¿Se recuperó la dependencia?

Si no puedes observar el breaker, no puedes ajustarlo. Para registros estructurados que se complementan bien con las métricas, consulta Registros Estructurados en Go con slog.

Una Forma de Cliente HTTP Más Amigable para Producción

Para servicios reales, evita dispersar la lógica del breaker de circuitos por los manejadores.

Crea un pequeño paquete de cliente alrededor de la dependencia.

Estructura de ejemplo:

internal/
  userservice/
    client.go
    errors.go
    metrics.go

El manejador no debe conocer los detalles de gobreaker. Debe depender de un método de cliente a nivel de dominio:

type UserService interface {
    GetUser(ctx context.Context, userID string) (*User, error)
}

Luego la implementación puede contener:

  • creación de solicitud HTTP
  • propagación de contexto
  • ejecución del breaker
  • manejo de códigos de estado
  • decodificación de respuesta
  • métricas
  • mapeo de errores

Esto mantiene la política de resiliencia cerca del límite de la dependencia. Para más información sobre la clasificación de errores en los límites, consulta Arquitectura de Manejo de Errores en Go: Límites y Patrones.

Dónde se Encajan los Breakers de Circuitos en la Arquitectura de la Aplicación

El patrón de breaker de circuitos pertenece en los límites de integración.

En una aplicación Go, eso usualmente significa:

graph LR A[Handler] --> B[Application Service] B --> C[Dependency Client] C --> D[Circuit Breaker] D --> E[HTTP / RPC / DB / Queue]

Mantén el breaker fuera de la lógica de negocio cuando sea posible.

La capa de negocio debe entender errores del dominio como:

proveedor de pagos no disponible
recomendaciones no disponibles
timeout del servicio de perfil

No necesita entender los estados de gobreaker.

Esta separación mantiene la arquitectura limpia:

  • los concerns de transporte se quedan en los clientes
  • la política de resiliencia se queda cerca de las dependencias
  • la lógica del dominio se mantiene legible
  • los manejadores se mantienen ligeros
  • las pruebas son más fáciles de escribir

Este artículo es parte del tema Arquitectura de Aplicaciones en Producción — junto con guías de idempotencia, outbox, saga y orquestación en Patrones de Integración.

Errores Comunes

Error 1: Sin Timeout

Un breaker de circuitos no detiene mágicamente las llamadas lentas a menos que las llamadas devuelvan.

Si la operación saliente puede colgarse para siempre, el breaker puede no ver un fallo lo suficientemente rápido.

Usa siempre timeouts.

Error 2: Un Breaker Global para Todo

No uses un solo breaker para todas las dependencias.

Un proveedor de correo electrónico fallido no debería abrir el circuito para tu proveedor de pagos. Un punto final de búsqueda lento no debería bloquear las llamadas al perfil del usuario.

Usa breakers separados para operaciones de dependencia separadas cuando sus modos de fallo difieran.

Error 3: Contar Errores del Cliente como Fallos de Dependencia

Si tu servicio envía una entrada incorrecta y recibe 400 Bad Request, eso usualmente no es una interrupción downstream.

No entrenes al breaker con tus propios errores de programación.

Error 4: Reintentar Escrituras No Idempotentes

Los reintentos no son gratuitos. Pueden duplicar escrituras, pagos, mensajes o efectos secundarios.

Haz que las escrituras sean idempotentes antes de reintentarlas.

Error 5: Ocultar Interrupciones Detrás de Fallbacks

Los fallbacks deben degradarse elegantemente, no falsificar la realidad.

Si una dependencia está caída, tus métricas y registros deberían hacer eso obvio.

Error 6: Ajustar Sin Datos de Producción

Los valores copiados de ejemplos son solo puntos de partida.

Ajusta basándote en:

  • volumen de solicitudes
  • tasa de error normal
  • latencia de la dependencia
  • impacto en el usuario
  • tiempo de recuperación
  • calidad del fallback

Error 7: Usar Breakers de Circuitos en lugar de Gestión de Capacidad

Un breaker de circuitos no es un sustituto de:

  • eliminación de carga (load shedding)
  • limitación de tasa
  • límites de cola
  • escalado automático
  • ajuste de base de datos
  • límites de pools de conexión
  • cuotas upstream

Es una parte de una estrategia de resiliencia.

Valores Predeterminados Prácticos

Para un servicio Go típico que llama a una dependencia HTTP interna, un punto de partida razonable podría ser:

HTTP client timeout: 2 a 5 segundos
timeout del contexto por solicitud: basado en la SLA del cliente
regla de fallo del breaker: 5 fallos consecutivos o 50 por ciento de fallo después de 20 solicitudes
timeout de apertura: 10 a 30 segundos
solicitudes semiabiertas: 1 a 5
conteo de reintentos: 1 a 3 intentos
backoff de reintento: exponencial con jitter

Estos no son valores universales. Son puntos de partida seguros.

Para APIs orientadas al usuario, mantén los presupuestos de latencia total ajustados. Para trabajos en segundo plano, puedes tolerar esperas más largas. Para proveedores de pagos, sea mucho más cuidadoso con los reintentos y la idempotencia.

Lista de Verificación de Breakers de Circuitos

Antes de añadir un breaker de circuitos, responde estas preguntas:

  • ¿Qué dependencia se está protegiendo?
  • ¿Qué operación se está protegiendo?
  • ¿Qué errores cuentan como fallo de dependencia?
  • ¿Qué errores deben ser ignorados por el breaker?
  • ¿Qué timeout aplica a cada llamada?
  • ¿Se permiten reintentos?
  • ¿Las escrituras son idempotentes?
  • ¿Qué sucede cuando el circuito está abierto?
  • ¿Hay un fallback?
  • ¿Es visible el fallback en las métricas?
  • ¿Quién recibe una alerta si el circuito sigue abriéndose?
  • ¿Cómo se ajustará el breaker después del despliegue?

Si no puedes responder a estas preguntas, añadir un breaker puede crear más confusión que resiliencia.

Probando Breakers de Circuitos en Go

Prueba el comportamiento, no la máquina de estados interna de la librería.

Las pruebas útiles incluyen:

  • la dependencia tiene éxito y la respuesta es devuelta
  • la dependencia falla repetidamente y el circuito se abre
  • el circuito abierto devuelve un error temporal
  • los errores de validación del lado del cliente no activan el breaker
  • el timeout del contexto se respeta
  • la respuesta de fallback es devuelta cuando se espera
  • las métricas son emitidas en cambios de estado

Usa servidores HTTP falsos para pruebas de integración:

server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    http.Error(w, "unavailable", http.StatusServiceUnavailable)
}))
defer server.Close()

Para pruebas unitarias, oculta la dependencia detrás de una interfaz e inyecta una implementación falsa.

Mantén las pruebas deterministas. Evita dormir por duraciones reales largas. Configura timeouts de breaker cortos en las pruebas. Para más información sobre la prueba de código concurrente en Go con tiempo falso y burbujas aisladas, consulta Probando Código Concurrente en Go con testing/synctest.

¿Deberías Construir tu Propio Breaker de Circuitos?

Construir un pequeño breaker de circuitos es un buen ejercicio de aprendizaje. Te ayuda a entender la máquina de estados.

Para código de producción, prefiere una librería mantenida a menos que tus necesidades sean muy específicas.

Un breaker de producción necesita manejar:

  • concurrencia
  • transiciones de estado
  • contadores
  • sondas semiabiertas
  • callbacks
  • clasificación personalizada de fallos
  • comportamiento libre de carreras (race-free)
  • manejo de errores predecible

Eso no es imposible, pero es fácil equivocarse sutilmente.

La librería aburrida suele ser la mejor opción.

Conclusión

El patrón de breaker de circuitos no es polvo de fiabilidad mágico.

En Go, funciona mejor cuando es parte de una pila de resiliencia pequeña y explícita:

timeout de contexto
+ reintento con backoff y jitter
+ breaker de circuitos
+ fallback
+ métricas

El patrón es más útil en los límites de las dependencias, especialmente alrededor de servicios remotos que pueden volverse lentos o parcialmente inaccessibles.

Úsalo para detener fallos en cascada. Úsalo para fallar rápido cuando una dependencia está claramente enferma. Úsalo para dar a los sistemas sobrecargados espacio para recuperarse.

Pero no lo uses como excusa para ignorar timeouts, idempotencia, observabilidad o arquitectura limpia.

Un buen breaker de circuitos hace el fallo más claro y barato. Uno malo solo hace el fallo más misterioso.

Referencias

Suscribirse

Recibe nuevas publicaciones sobre sistemas, infraestructura e ingeniería de IA.