Patrón Circuit Breaker Half-Open
Cómo testear service recovery con half-open circuit breaker state transitions. Cubre closed, open, half-open states, trial requests, y gradual recovery.
Nota para desarrolladores hispanohablantes: Esta guía incluye ejemplos y convenciones de nomenclatura adaptadas a equipos que trabajan en español. Cuando existen diferencias significativas en terminología técnica entre el inglés y el español, se indican explícitamente para facilitar la comunicación en equipos multiculturales.
Overview
El circuit breaker half-open pattern extiende el basic circuit breaker con un half-open state para testear recovery. Un circuit breaker tiene tres states: closed (requests flowean normally), open (requests son rejected immediately), y half-open (un limited number de trial requests son allowed). Cuando el circuit transitionéa de open a half-open, sendéa un small number de test requests. Si succeeden, el circuit se cierra y full traffic resume. Si fallan, el circuit se reopen. Esto previene floodear un recovering service con full traffic, lo cual podría causar que falle again. El half-open state es el critical bridge entre failure detection y full recovery.
When to Use
- Protecteando services de cascading failures durante outages
- Llamando downstream services que pueden volverse temporarily unavailable
- Microservices donde un service failure no debería cascadear a todos los callers
- Database connection management con automatic recovery
- External API calls que pueden experimentar transient outages
When NOT to Use
- In-process function calls (usá try/catch)
- Operations donde el failure es permanent (validation errors, 404s)
- Real-time systems donde el open-state rejection latency es unacceptable
- Cuando necesitás que cada request llegue al service (no fail-fast behavior)
Solution
Three-state circuit breaker (Python)
# circuit_breaker/three_state.py — Circuit breaker con half-open state
import time
import threading
from enum import Enum
from collections import defaultdict
class CircuitState(Enum):
CLOSED = "closed"
OPEN = "open"
HALF_OPEN = "half_open"
class CircuitBreaker:
"""Circuit breaker con closed, open, y half-open states.
Closed: requests flowean normally. Failures incrementan el counter.
Open: requests son rejected immediately. Después de timeout, transitionéa a half-open.
Half-open: limited trial requests. Success cierra, failure reopen.
"""
def __init__(self, failure_threshold=5, recovery_timeout=30,
half_open_max_calls=3, success_threshold=2):
self.failure_threshold = failure_threshold
self.recovery_timeout = recovery_timeout
self.half_open_max_calls = half_open_max_calls
self.success_threshold = success_threshold
self._state = CircuitState.CLOSED
self._failure_count = 0
self._success_count = 0
self._half_open_calls = 0
self._last_failure_time = 0
self._lock = threading.Lock()
@property
def state(self):
with self._lock:
self._check_state_transition()
return self._state
def _check_state_transition(self):
"""Checkeá si el circuit debería transitionear de open a half-open."""
if self._state == CircuitState.OPEN:
if time.time() - self._last_failure_time >= self.recovery_timeout:
self._state = CircuitState.HALF_OPEN
self._half_open_calls = 0
self._success_count = 0
print(f"Circuit transitioned: OPEN -> HALF_OPEN")
def call(self, fn, *args, **kwargs):
"""Ejecutá fn a través del circuit breaker."""
with self._lock:
self._check_state_transition()
if self._state == CircuitState.OPEN:
raise CircuitBreakerOpenError(
"Circuit is open — service unavailable"
)
if self._state == CircuitState.HALF_OPEN:
if self._half_open_calls >= self.half_open_max_calls:
raise CircuitBreakerOpenError(
"Circuit is half-open — trial request limit reached"
)
self._half_open_calls += 1
# Ejecutá el function fuera del lock
try:
result = fn(*args, **kwargs)
self._on_success()
return result
except Exception as e:
self._on_failure()
raise
def _on_success(self):
with self._lock:
if self._state == CircuitState.HALF_OPEN:
self._success_count += 1
if self._success_count >= self.success_threshold:
self._state = CircuitState.CLOSED
self._failure_count = 0
print(f"Circuit transitioned: HALF_OPEN -> CLOSED")
elif self._state == CircuitState.CLOSED:
self._failure_count = 0
def _on_failure(self):
with self._lock:
self._last_failure_time = time.time()
if self._state == CircuitState.HALF_OPEN:
self._state = CircuitState.OPEN
self._failure_count = 0
print(f"Circuit transitioned: HALF_OPEN -> OPEN")
elif self._state == CircuitState.CLOSED:
self._failure_count += 1
if self._failure_count >= self.failure_threshold:
self._state = CircuitState.OPEN
print(f"Circuit transitioned: CLOSED -> OPEN "
f"(failures: {self._failure_count})")
def get_state(self):
with self._lock:
self._check_state_transition()
return {
"state": self._state.value,
"failure_count": self._failure_count,
"success_count": self._success_count,
"half_open_calls": self._half_open_calls
}
def reset(self):
with self._lock:
self._state = CircuitState.CLOSED
self._failure_count = 0
self._success_count = 0
self._half_open_calls = 0
class CircuitBreakerOpenError(Exception):
"""Raiseado cuando el circuit está open y requests son rejected."""
pass
# Usage
breaker = CircuitBreaker(
failure_threshold=5,
recovery_timeout=30,
half_open_max_calls=3,
success_threshold=2
)
def call_payment_service(order_id):
return breaker.call(lambda: payment_api.charge(order_id))
Per-service circuit breaker registry (Python)
# circuit_breaker/registry.py — Manageéa circuit breakers per service
import threading
from circuit_breaker.three_state import CircuitBreaker, CircuitState
class CircuitBreakerRegistry:
"""Maintainéa un circuit breaker per service name.
Cada service tiene independent state y thresholds."""
def __init__(self):
self._breakers = {}
self._lock = threading.Lock()
def get_or_create(self, service_name, failure_threshold=5,
recovery_timeout=30, half_open_max_calls=3,
success_threshold=2):
with self._lock:
if service_name not in self._breakers:
self._breakers[service_name] = CircuitBreaker(
failure_threshold=failure_threshold,
recovery_timeout=recovery_timeout,
half_open_max_calls=half_open_max_calls,
success_threshold=success_threshold
)
return self._breakers[service_name]
def call(self, service_name, fn, *args, **kwargs):
breaker = self.get_or_create(service_name)
return breaker.call(fn, *args, **kwargs)
def get_all_states(self):
with self._lock:
return {
name: breaker.get_state()
for name, breaker in self._breakers.items()
}
def reset_all(self):
with self._lock:
for breaker in self._breakers.values():
breaker.reset()
# Usage
registry = CircuitBreakerRegistry()
def call_payment(order_id):
return registry.call("payment", payment_api.charge, order_id)
def call_inventory(product_id):
return registry.call("inventory", inventory_api.check, product_id)
# Monitoreá todos los circuits
for name, state in registry.get_all_states().items():
print(f"{name}: {state['state']}")
Java circuit breaker con resilience4j
// CircuitBreakerConfig.java — Java circuit breaker con resilience4j
import io.github.resilience4j.circuitbreaker.CircuitBreaker;
import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig;
import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry;
import java.time.Duration;
public class ServiceCircuitBreakers {
private final CircuitBreakerRegistry registry;
public ServiceCircuitBreakers() {
CircuitBreakerConfig config = CircuitBreakerConfig.custom()
.failureRateThreshold(50) // 50% failure rate abre circuit
.slowCallRateThreshold(80) // 80% slow calls abre circuit
.slowCallDurationThreshold(Duration.ofSeconds(5))
.waitDurationInOpenState(Duration.ofSeconds(30))
.slidingWindowSize(10) // Last 10 calls
.minimumNumberOfCalls(5) // Need 5 calls antes de evaluar
.permittedNumberOfCallsInHalfOpenState(3) // Trial calls en half-open
.waitDurationInHalfOpenState(Duration.ofSeconds(10))
.automaticTransitionFromOpenToHalfOpenEnabled(true)
.build();
this.registry = CircuitBreakerRegistry.of(config);
}
public <T> T callWithBreaker(String serviceName, java.util.function.Supplier<T> supplier) {
CircuitBreaker breaker = registry.circuitBreaker(serviceName);
return breaker.executeSupplier(supplier);
}
public String callPayment(String orderId) {
return callWithBreaker("payment", () -> paymentClient.charge(orderId));
}
public String callInventory(String productId) {
return callWithBreaker("inventory", () -> inventoryClient.checkStock(productId));
}
public void printStates() {
registry.getAllCircuitBreakers().forEach(breaker -> {
var state = breaker.getState();
var metrics = breaker.getMetrics();
System.out.printf("%s: state=%s, failures=%d, success=%d%n",
breaker.getName(),
state,
metrics.getNumberOfFailedCalls(),
metrics.getNumberOfSuccessfulCalls());
});
}
}
Circuit breaker con metrics (Python)
# circuit_breaker/with_metrics.py — Circuit breaker con Prometheus metrics
import time
import threading
from prometheus_client import Counter, Gauge, Histogram
from circuit_breaker.three_state import CircuitBreaker, CircuitState
class InstrumentedCircuitBreaker(CircuitBreaker):
"""Circuit breaker que exportéa metrics a Prometheus."""
_state_gauges = {}
_call_counters = {}
_call_latency = {}
def __init__(self, name, **kwargs):
super().__init__(**kwargs)
self.name = name
if name not in self._state_gauges:
self._state_gauges[name] = Gauge(
f"circuit_breaker_state_{name}",
f"Circuit breaker state for {name} (0=closed, 1=open, 2=half_open)"
)
self._call_counters[name] = Counter(
f"circuit_breaker_calls_{name}",
f"Calls through circuit breaker {name}",
["result"]
)
self._call_latency[name] = Histogram(
f"circuit_breaker_latency_{name}",
f"Call latency through circuit breaker {name}"
)
def call(self, fn, *args, **kwargs):
state_map = {
CircuitState.CLOSED: 0,
CircuitState.OPEN: 1,
CircuitState.HALF_OPEN: 2
}
self._state_gauges[self.name].set(state_map.get(self.state, 0))
try:
start = time.time()
result = super().call(fn, *args, **kwargs)
latency = time.time() - start
self._call_counters[self.name].labels(result="success").inc()
self._call_latency[self.name].observe(latency)
return result
except CircuitBreakerOpenError:
self._call_counters[self.name].labels(result="rejected").inc()
raise
except Exception as e:
self._call_counters[self.name].labels(result="failure").inc()
raise
JavaScript circuit breaker
// circuit_breaker/index.js — Circuit breaker para Node.js
class CircuitBreaker {
constructor(options = {}) {
this.failureThreshold = options.failureThreshold || 5;
this.recoveryTimeout = options.recoveryTimeout || 30000;
this.halfOpenMaxCalls = options.halfOpenMaxCalls || 3;
this.successThreshold = options.successThreshold || 2;
this.state = "closed";
this.failureCount = 0;
this.successCount = 0;
this.halfOpenCalls = 0;
this.lastFailureTime = 0;
}
async call(fn, ...args) {
this._checkStateTransition();
if (this.state === "open") {
throw new Error("Circuit is open — service unavailable");
}
if (this.state === "half-open") {
if (this.halfOpenCalls >= this.halfOpenMaxCalls) {
throw new Error("Circuit is half-open — trial limit reached");
}
this.halfOpenCalls++;
}
try {
const result = await fn(...args);
this._onSuccess();
return result;
} catch (error) {
this._onFailure();
throw error;
}
}
_checkStateTransition() {
if (this.state === "open") {
if (Date.now() - this.lastFailureTime >= this.recoveryTimeout) {
this.state = "half-open";
this.halfOpenCalls = 0;
this.successCount = 0;
console.log("Circuit: OPEN -> HALF_OPEN");
}
}
}
_onSuccess() {
if (this.state === "half-open") {
this.successCount++;
if (this.successCount >= this.successThreshold) {
this.state = "closed";
this.failureCount = 0;
console.log("Circuit: HALF_OPEN -> CLOSED");
}
} else if (this.state === "closed") {
this.failureCount = 0;
}
}
_onFailure() {
this.lastFailureTime = Date.now();
if (this.state === "half-open") {
this.state = "open";
console.log("Circuit: HALF_OPEN -> OPEN");
} else if (this.state === "closed") {
this.failureCount++;
if (this.failureCount >= this.failureThreshold) {
this.state = "open";
console.log(`Circuit: CLOSED -> OPEN (${this.failureCount} failures)`);
}
}
}
getState() {
return {
state: this.state,
failureCount: this.failureCount,
successCount: this.successCount,
halfOpenCalls: this.halfOpenCalls
};
}
}
// Usage
const breaker = new CircuitBreaker({
failureThreshold: 5,
recoveryTimeout: 30000,
halfOpenMaxCalls: 3,
successThreshold: 2
});
async function callApi(url) {
return breaker.call(async () => {
const response = await fetch(url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json();
}, url);
}
Variants
Gradual recovery (ramped half-open)
# circuit_breaker/gradual.py — Gradualmente aumentá traffic en half-open
class GradualCircuitBreaker(CircuitBreaker):
"""En vez de binary closed/half-open, gradualmente aumentá allowed traffic.
Half-open phase 1: 10% de requests
Half-open phase 2: 50% de requests
Half-open phase 3: 100% (closed)"""
PHASES = [
{"name": "phase1", "ratio": 0.1, "success_needed": 5},
{"name": "phase2", "ratio": 0.5, "success_needed": 10},
{"name": "phase3", "ratio": 1.0, "success_needed": 20},
]
def __init__(self, **kwargs):
super().__init__(**kwargs)
self._phase = 0
self._phase_success = 0
def call(self, fn, *args, **kwargs):
import random
if self.state == CircuitState.HALF_OPEN:
phase = self.PHASES[self._phase]
if random.random() > phase["ratio"]:
raise CircuitBreakerOpenError(
f"Half-open {phase['name']}: only {phase['ratio']*100}% traffic"
)
try:
result = super().call(fn, *args, **kwargs)
if self.state == CircuitState.HALF_OPEN:
self._phase_success += 1
if self._phase_success >= phase["success_needed"]:
self._phase = min(self._phase + 1, len(self.PHASES) - 1)
if self._phase == len(self.PHASES) - 1:
self._state = CircuitState.CLOSED
return result
except Exception:
self._phase = 0
self._phase_success = 0
raise
Circuit breaker con fallback
# circuit_breaker/with_fallback.py — Returneá fallback cuando circuit está open
class CircuitBreakerWithFallback:
"""Cuando el circuit está open, returneá un fallback response
en vez de raiseear un error."""
def __init__(self, breaker, fallback_fn):
self._breaker = breaker
self._fallback = fallback_fn
def call(self, fn, *args, **kwargs):
try:
return self._breaker.call(fn, *args, **kwargs)
except CircuitBreakerOpenError:
return self._fallback(*args, **kwargs)
# Usage
breaker = CircuitBreaker(failure_threshold=5, recovery_timeout=30)
def fallback_product(product_id):
return {"id": product_id, "name": "Unavailable", "cached": True}
protected = CircuitBreakerWithFallback(
breaker,
fallback_fn=fallback_product
)
def get_product(product_id):
return protected.call(inventory_api.get_product, product_id)
Best Practices
-
For a deeper guide, see Graceful Degradation Pattern.
-
Seteá appropriate thresholds — too low causa false opens; too high allowéa cascading failures
-
Usá sliding window para failure counting — no un simple counter que never resetéa
-
Monitoreá circuit state transitions — alertéa en frequent open/close oscillation
-
Usá el half-open state — previene floodear un recovering service
-
Seteá un success threshold en half-open — requireé N consecutive successes antes de cerrar
-
Combiná con fallbacks — returneá cached/default data cuando el circuit está open
-
Exportéa metrics — trackeá state, failure rate, rejection count per circuit
-
Testeá el circuit breaker — injectéa failures y verificá state transitions
Common Mistakes
- No half-open state: circuit oscillate entre open y closed, floodeando el recovering service.
- Failure threshold too low: circuit abre en un single failure, rejecteando valid traffic.
- No success threshold: first half-open success cierra el circuit, pero el service puede no estar fully recovered.
- Not resetting failure count en success: una vez que failures accumulate, el circuit never se cierra.
- No monitoring: no sabés que los circuits están abriendo hasta que users se quejan.
FAQ
¿Qué es el half-open state en un circuit breaker?
Un transitional state entre open y closed. El circuit allowéa un limited number de trial requests. Si succeeden, el circuit se cierra y full traffic resume. Si fallan, el circuit se reopen. Esto previene floodear un recovering service con full traffic.
¿Cuántos trial requests debería allowear en half-open?
Típicamente 3-5. Too few y un single failed trial reopenéa el circuit. Too many y riskéas overwhelming el recovering service. Requireé 2-3 consecutive successes antes de cerrar el circuit.
¿Cuál es la diferencia entre failure rate y failure count thresholds?
Failure count: circuit abre después de N failures sin importar total calls. Failure rate: circuit abre cuando X% de los last N calls fallearon. Rate-based es más adaptive — 5 failures out of 100 calls está fine; 5 out of 5 no.
¿Debería el circuit breaker timeout ser fixed o exponential?
Arrancá con un fixed timeout (30-60 seconds). Si el service recoveréa quick, el circuit abre unnecessarily. Si recoveréa slow, trial requests fallan y reopenéan el circuit. Para services con unpredictable recovery, usá exponential backoff en el timeout.
¿Puedo usar circuit breaker con async/await?
Sí. El circuit breaker logic (state checks, transitions) es synchronous. El actual function call puede ser async. En JavaScript, wrapeá el async call en breaker.call(asyncFn). En Python, llamá breaker.call(asyncio.run, async_fn).
Recursos Relacionados
Bulkhead Pattern: Isolate Resources to Limit Blast Radius
How to isolate resources per service to limit blast radius. Covers thread pool isolation, connection pool partitioning, semaphore-based bulkheads, and resource quotas.
PatternRetry with Jitter: Exponential Backoff and Random Jitter
How to retry failed operations with exponential backoff and random jitter. Covers full jitter, equal jitter, decorrelated jitter, retry budgets, and idempotency.
PatternGraceful Shutdown: Drain In-Flight Requests Before Exit
How to drain in-flight requests before process exit. Covers signal handling, health check removal, connection draining, timeout enforcement, and cleanup hooks.
PatternFallover: Switch to Standby on Primary Failure Detection
How to switch to a standby system on primary failure detection. Covers active-passive, active-active, health monitoring, DNS fallover, database replication, and automated promotion.
PatternToken Bucket Rate Limiter: Smooth Traffic with Token Buckets
How to implement token bucket rate limiting for API protection. Covers bucket refill, burst handling, per-key buckets, distributed rate limiting with Redis, and sliding windows.