HTTP Circuit Breaker — Complete Implementation Guide
In this tutorial, you will learn about HTTP Circuit Breaker. We cover key concepts, practical examples, and best practices to help you master this topic.
HTTP circuit breaker protects HTTP clients from failing downstream services by wrapping HTTP calls, tracking response statuses, and opening the circuit when errors exceed thresholds.
What You'll Learn
By the end of this tutorial, you will implement circuit breakers around HTTP calls using fetch and axios, configure per-endpoint circuits, and provide fallback responses.
Real-World Use
DodaTech's API Gateway wraps every downstream HTTP call with a circuit breaker. When a service fails, the gateway returns a cached response instead of an error.
HTTP Circuit Breaker Implementation
const express = require("express");
const app = express();
class HTTPCircuitBreaker {
constructor(options = {}) {
this.threshold = options.threshold || 5;
this.resetTimeout = options.resetTimeout || 30000;
this.timeout = options.timeout || 5000;
this.state = "closed";
this.failureCount = 0;
this.nextAttempt = Date.now();
}
async fetch(url, options = {}) {
if (this.state === "open") {
if (Date.now() < this.nextAttempt) {
throw Object.assign(new Error("CircuitBreakerOpen"), { code: "CB_OPEN" });
}
this.state = "half-open";
}
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), this.timeout);
try {
const response = await fetch(url, {
...options,
signal: controller.signal
});
if (!response.ok && response.status >= 500) {
throw new Error(`HTTP ${response.status}`);
}
this.onSuccess();
return response;
} catch (err) {
this.onFailure(err);
throw err;
} finally {
clearTimeout(timer);
}
}
onSuccess() {
this.failureCount = 0;
if (this.state === "half-open") this.state = "closed";
}
onFailure(err) {
this.failureCount++;
if (this.state === "half-open" || this.failureCount >= this.threshold) {
this.state = "open";
this.nextAttempt = Date.now() + this.resetTimeout;
}
}
}
Axios Circuit Breaker Interceptor
Integrate circuit breakers with axios using request interceptors for seamless protection.
const axios = require("axios");
class AxiosCircuitBreaker {
constructor(name, options = {}) {
this.name = name;
this.threshold = options.threshold || 5;
this.resetTimeout = options.resetTimeout || 30000;
this.state = "closed";
this.failures = 0;
this.nextAttempt = Date.now();
}
getInterceptor() {
return {
request: (config) => {
if (this.state === "open") {
if (Date.now() < this.nextAttempt) {
return Promise.reject(new Error(`CB_OPEN: ${this.name}`));
}
this.state = "half-open";
}
config.cbBreaker = this;
return config;
},
response: (response) => {
const breaker = response.config.cbBreaker;
if (breaker) {
breaker.failures = 0;
if (breaker.state === "half-open") breaker.state = "closed";
}
return response;
},
responseError: (error) => {
const breaker = error.config?.cbBreaker;
if (breaker) {
breaker.failures++;
if (breaker.state === "half-open" || breaker.failures >= breaker.threshold) {
breaker.state = "open";
breaker.nextAttempt = Date.now() + breaker.resetTimeout;
}
}
return Promise.reject(error);
}
};
}
}
const userServiceCB = new AxiosCircuitBreaker("user-service");
const httpClient = axios.create({ baseURL: "https://user-service/api" });
httpClient.interceptors.request.use(userServiceCB.getInterceptor().request);
httpClient.interceptors.response.use(
userServiceCB.getInterceptor().response,
userServiceCB.getInterceptor().responseError
);
Per-Endpoint Circuit Breakers
Different API endpoints need independent circuit breakers with different thresholds.
class HTTPCircuitBreakerRegistry {
constructor() {
this.breakers = new Map();
}
for(url, options = {}) {
const key = new URL(url).hostname;
if (!this.breakers.has(key)) {
this.breakers.set(key, new HTTPCircuitBreaker(options));
}
return this.breakers.get(key);
}
async fetch(url, options = {}) {
const breaker = this.for(url, options);
return breaker.fetch(url, options);
}
}
const registry = new HTTPCircuitBreakerRegistry();
app.get("/api/users", async (req, res) => {
try {
const response = await registry.fetch("https://user-service/users");
const data = await response.json();
res.json(data);
} catch (err) {
if (err.code === "CB_OPEN") {
return res.status(503).json({ error: "Service unavailable", cached: true, data: [] });
}
res.status(502).json({ error: "Bad gateway" });
}
});
app.get("/api/orders", async (req, res) => {
try {
const response = await registry.fetch("https://order-service/orders", {
threshold: 3,
resetTimeout: 15000
});
const data = await response.json();
res.json(data);
} catch (err) {
if (err.code === "CB_OPEN") {
return res.status(503).json({ error: "Order service unavailable" });
}
res.status(502).json({ error: "Bad gateway" });
}
});
Common Mistakes
Using a single circuit breaker for all HTTP calls -- Different hosts have different reliability. Create per-host circuits.
Not distinguishing HTTP errors -- 4xx errors should not open the circuit. Only 5xx and network errors.
No timeout on HTTP calls -- Without timeouts, a slow service keeps the circuit closed while requests pile up.
Not handling DNS failures -- DNS failures should count toward the circuit breaker threshold.
Creating circuit breakers per request -- Circuit breakers must persist. Store them in a registry.
Practice Questions
Why use per-host circuit breakers instead of a single global one? Different services have different reliability. A failure in one should not affect calls to healthy services.
Which HTTP status codes should trigger the circuit breaker? 5xx server errors and network errors. 4xx client errors should not.
How does the circuit breaker handle timeouts differently from other errors? Timeouts are a strong signal of trouble. They should always count toward the failure threshold.
Challenge: Implement a circuit breaker that distinguishes between read and write endpoints.
class ReadWriteCircuitBreaker {
constructor() {
this.read = new HTTPCircuitBreaker({ threshold: 5 });
this.write = new HTTPCircuitBreaker({ threshold: 3 });
}
async fetch(url, options) {
const method = options.method || "GET";
const breaker = method === "GET" ? this.read : this.write;
return breaker.fetch(url, options);
}
}
FAQ
Mini Project
Build an HTTP circuit breaker client with per-host circuits, configurable thresholds, timeout handling, and Prometheus metrics integration.
class HTTPCircuitBreakerClient {
constructor() {
this.breakers = new Map();
}
getBreaker(host) {
if (!this.breakers.has(host)) {
this.breakers.set(host, {
state: "closed",
failures: 0,
threshold: 5,
resetTimeout: 30000,
nextAttempt: Date.now()
});
}
return this.breakers.get(host);
}
async request(url, options = {}) {
const host = new URL(url).hostname;
const breaker = this.getBreaker(host);
if (breaker.state === "open") {
if (Date.now() < breaker.nextAttempt) {
return { error: "circuit_open", cached: true };
}
breaker.state = "half-open";
}
try {
const response = await fetch(url, {
...options,
signal: AbortSignal.timeout(options.timeout || 5000)
});
if (!response.ok && response.status >= 500) {
throw new Error(`HTTP ${response.status}`);
}
breaker.state = "closed";
breaker.failures = 0;
return response;
} catch (err) {
breaker.failures++;
if (breaker.state === "half-open" || breaker.failures >= breaker.threshold) {
breaker.state = "open";
breaker.nextAttempt = Date.now() + breaker.resetTimeout;
}
return { error: err.message };
}
}
}
What's Next
Now that you understand HTTP circuit breakers, explore circuit breakers for database connections. Then learn about circuit breakers in microservice architectures.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro