export type CircuitState = "closed" | "open" | "half-open";
export interface CircuitBreakerOptions {
failureThreshold: number;
resetTimeout: number; // ms to wait in "open" before probing
onStateChange?: (from: CircuitState, to: CircuitState) => void;
}
export class CircuitOpenError extends Error {
constructor(public readonly retryAfterMs: number) {
super(`Circuit is open; retry in ${retryAfterMs}ms`);
this.name = "CircuitOpenError";
}
}
import { CircuitBreakerOptions, CircuitOpenError, CircuitState } from "./types";
export class CircuitBreaker {
private state: CircuitState = "closed";
private failures = 0;
private openedAt = 0;
constructor(private readonly opts: CircuitBreakerOptions) {}
async execute<T>(fn: () => Promise<T>): Promise<T> {
if (this.state === "open") {
const elapsed = Date.now() - this.openedAt;
if (elapsed < this.opts.resetTimeout) {
throw new CircuitOpenError(this.opts.resetTimeout - elapsed);
}
this.transition("half-open");
}
try {
const result = await fn();
this.onSuccess();
return result;
} catch (err) {
this.onFailure();
throw err;
}
}
private onSuccess(): void {
this.failures = 0;
if (this.state !== "closed") this.transition("closed");
}
private onFailure(): void {
this.failures += 1;
if (this.state === "half-open" || this.failures >= this.opts.failureThreshold) {
this.openedAt = Date.now();
this.transition("open");
}
}
private transition(to: CircuitState): void {
if (to === this.state) return;
const from = this.state;
this.state = to;
if (to === "closed") this.failures = 0;
this.opts.onStateChange?.(from, to);
}
}
import { CircuitBreaker } from "./CircuitBreaker";
export interface Charge {
id: string;
status: "pending" | "succeeded" | "failed";
}
export class PaymentApiClient {
private readonly breaker: CircuitBreaker;
constructor(private readonly baseUrl: string, private readonly apiKey: string) {
this.breaker = new CircuitBreaker({
failureThreshold: 5,
resetTimeout: 30_000,
onStateChange: (from, to) =>
console.warn(`[payments] circuit ${from} -> ${to}`),
});
}
createCharge(amountCents: number, currency: string): Promise<Charge> {
return this.breaker.execute(() =>
this.request<Charge>("POST", "/v1/charges", { amountCents, currency }),
);
}
getCharge(id: string): Promise<Charge> {
return this.breaker.execute(() =>
this.request<Charge>("GET", `/v1/charges/${id}`),
);
}
private async request<T>(method: string, path: string, body?: unknown): Promise<T> {
const res = await this.withTimeout(5_000, (signal) =>
fetch(`${this.baseUrl}${path}`, {
method,
signal,
headers: {
Authorization: `Bearer ${this.apiKey}`,
"Content-Type": "application/json",
},
body: body ? JSON.stringify(body) : undefined,
}),
);
if (!res.ok) {
throw new Error(`payment api ${res.status} on ${method} ${path}`);
}
return (await res.json()) as T;
}
private async withTimeout<T>(ms: number, fn: (signal: AbortSignal) => Promise<T>): Promise<T> {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), ms);
try {
return await fn(controller.signal);
} finally {
clearTimeout(timer);
}
}
}
A circuit breaker protects a service from a downstream dependency that has started failing or timing out. Rather than sending every request into a black hole and exhausting connection pools or thread budgets, the breaker watches the recent failure rate and, once it crosses a threshold, "opens" — short-circuiting further calls with an immediate error until the dependency has had time to recover. This snippet shows a small, self-contained implementation and how it wraps a real HTTP client.
The CircuitBreaker tab holds the state machine. It tracks three states — closed (traffic flows), open (calls are rejected fast), and half-open (a single probe is allowed through to test recovery). execute is the entry point: when the breaker is open, it checks whether resetTimeout has elapsed and either transitions to half-open or throws a CircuitOpenError without ever touching the network. Successes and failures feed onSuccess/onFailure, which use a rolling failures count and a failureThreshold to decide when to trip. A single failure in half-open re-opens the circuit, which is deliberate: recovery should be proven, not assumed.
The key trade-off is the sliding threshold versus a rate-based window. This version counts consecutive-ish failures for simplicity, which is easy to reason about but can be noisy under bursty traffic; a production system often layers in a percentage over a time window. Note also that the breaker only guards against the failures it can see — a timeout must be enforced by the caller, which is why the client below wraps fetch in an AbortController.
The PaymentApiClient tab shows the realistic usage. Each public method routes its network call through breaker.execute, so retries and timeouts live in one place. withTimeout races the request against an abort signal, and non-2xx responses are thrown so the breaker counts them as failures rather than silently succeeding. The types tab defines the shared options and the typed error so callers can distinguish a genuinely-open circuit from a normal request failure and, for example, serve cached data instead. This pattern is worth reaching for whenever a dependency is flaky enough that failing fast is cheaper than waiting on it.
Related snips
package api
import (
"net/http"
"runtime/debug"
)
Expose build metadata for debugging deploys
export interface RetryOptions {
retries: number;
baseMs: number;
maxMs: number;
signal?: AbortSignal;
onRetry?: (attempt: number, delay: number, err: unknown) => void;
Exponential backoff with jitter for retries
use tracing::{info, instrument};
#[instrument]
fn process_request(user_id: u64) {
info!(user_id, "Processing request");
// Work happens here
tracing for structured logging and distributed tracing
import axios, { AxiosError } from 'axios'
import { v4 as uuidv4 } from 'uuid'
const api = axios.create({
baseURL: import.meta.env.VITE_API_URL || 'http://localhost:3000/api/v1',
timeout: 15000,
Axios API client with interceptors
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
interface FilterState {
search: string
category: string | null
Zustand for lightweight state management
package deps
import (
"net"
"net/http"
"time"
HTTP client tuned for production: timeouts, transport, and connection reuse
Share this code
Here's the card — post it anywhere.