Skip to content

Circuit Breaker Implementation — Complete Node.js Guide

DodaTech Updated 2026-06-28 5 min read

In this tutorial, you will learn about Circuit Breaker Implementation. We cover key concepts, practical examples, and best practices to help you master this topic.

Circuit breaker implementation in Node.js involves building a class that wraps function calls, tracks successes and failures, manages state transitions, and integrates with Express middleware.

What You'll Learn

By the end of this tutorial, you will implement a production-ready circuit breaker in Node.js with event emitters, configurable thresholds, and Express middleware integration.

Why It Matters

A well-implemented circuit breaker is reusable across all service calls. DodaTech's Node.js services share a common circuit breaker implementation with consistent behavior.

Real-World Use

DodaTech's Node.js API Gateway uses circuit breakers around every downstream service call with configurable thresholds per service.

Implementation Learning Path

flowchart LR
  A[States] --> B[Implementation]
  B --> C[Circuit Breaker Class]
  C --> D[Express Middleware]
  B --> E{You Are Here}
  style E fill:#f90,color:#fff

Basic Circuit Breaker Class

A reusable circuit breaker class that wraps async functions with state management.

const EventEmitter = require("events");

class CircuitBreaker extends EventEmitter {
  constructor(options = {}) {
    super();
    this.threshold = options.threshold || 5;
    this.resetTimeout = options.resetTimeout || 30000;
    this.state = "closed";
    this.failureCount = 0;
    this.nextAttempt = Date.now();
  }

  async call(fn) {
    if (this.state === "open") {
      if (Date.now() < this.nextAttempt) {
        this.emit("reject", { time: Date.now() });
        throw new Error("CircuitBreakerOpen");
      }
      this.transition("half-open");
    }

    try {
      const result = await fn();
      this.onSuccess();
      return result;
    } catch (err) {
      this.onFailure(err);
      throw err;
    }
  }

  onSuccess() {
    this.failureCount = 0;
    if (this.state === "half-open") {
      this.transition("closed");
    }
  }

  onFailure(err) {
    this.failureCount++;
    this.emit("failure", { count: this.failureCount, error: err.message });

    if (this.state === "half-open" || this.failureCount >= this.threshold) {
      this.transition("open");
      this.nextAttempt = Date.now() + this.resetTimeout;
    }
  }

  transition(state) {
    const prev = this.state;
    this.state = state;
    this.emit("transition", { from: prev, to: state, time: Date.now() });
  }
}

module.exports = CircuitBreaker;

Express Middleware Integration

Wrap Express route handlers with circuit breaker protection for downstream HTTP calls.

const express = require("express");
const CircuitBreaker = require("./circuit-breaker");
const app = express();

const downstreamCB = new CircuitBreaker({ threshold: 3, resetTimeout: 15000 });

downstreamCB.on("transition", (event) => {
  console.log(`Downstream service: ${event.from} -> ${event.to}`);
});

downstreamCB.on("reject", () => {
  console.warn("Request rejected: downstream circuit is open");
});

async function callDownstreamService() {
  const response = await fetch("http://downstream-api/data");
  if (!response.ok) throw new Error(`HTTP ${response.status}`);
  return response.json();
}

async function circuitBreakingHandler(req, res, next) {
  try {
    const data = await downstreamCB.call(callDownstreamService);
    res.json(data);
  } catch (err) {
    if (err.message === "CircuitBreakerOpen") {
      return res.status(503).json({
        error: "Service temporarily unavailable",
        retryAfter: Math.ceil((downstreamCB.nextAttempt - Date.now()) / 1000)
      });
    }
    next(err);
  }
}

app.get("/api/data", circuitBreakingHandler);

app.listen(3000);

Per-Endpoint Circuit Breakers

Different endpoints may share the same downstream service but need independent circuit breakers for different operations.

class CircuitBreakerRegistry {
  constructor() {
    this.breakers = new Map();
  }

  get(name, options = {}) {
    if (!this.breakers.has(name)) {
      this.breakers.set(name, new CircuitBreaker(options));
    }
    return this.breakers.get(name);
  }

  getAllStates() {
    const states = {};
    for (const [name, cb] of this.breakers) {
      states[name] = { state: cb.state, failures: cb.failureCount };
    }
    return states;
  }
}

const registry = new CircuitBreakerRegistry();

app.get("/api/users", async (req, res) => {
  const cb = registry.get("user-service", { threshold: 5 });
  try {
    const users = await cb.call(() => fetchUsers());
    res.json(users);
  } catch (err) {
    if (err.message === "CircuitBreakerOpen") {
      return res.status(503).json({ error: "User service unavailable" });
    }
    res.status(500).json({ error: err.message });
  }
});

app.get("/api/orders", async (req, res) => {
  const cb = registry.get("order-service", { threshold: 3 });
  // Similar pattern
});

Common Mistakes

  1. Not emitting events -- Without events, you cannot monitor circuit breaker state. Always emit state change events.

  2. Creating a new circuit breaker per request -- Circuit breakers must persist across requests to track failure history.

  3. Not handling half-open failures correctly -- A half-open failure should reopen immediately, not increment the closed threshold.

  4. Blocking the event loop -- Circuit breaker decision logic is synchronous and fast. Keep it that way.

  5. Forgetting to reset failure count on close -- When recovering from half-open to closed, failure count must reset to zero.

Practice Questions

  1. Why use EventEmitter for circuit breaker notifications? It allows decoupled monitoring, logging, and alerting without modifying the circuit breaker code.

  2. How do per-endpoint circuit breakers share state? A registry manages named circuit breaker instances. Each endpoint accesses its own instance.

  3. What happens to in-flight requests when the circuit opens? They continue running. Only new requests are rejected. The circuit breaker tracks failures from completed requests.

  4. Challenge: Implement a circuit breaker that falls back to cached data when open.

async function callWithCachedFallback(cb, fn, cacheKey, cacheStore) {
  try {
    const result = await cb.call(fn);
    cacheStore.set(cacheKey, result);
    return result;
  } catch (err) {
    if (err.message === "CircuitBreakerOpen") {
      return cacheStore.get(cacheKey) || { error: "No cached data" };
    }
    throw err;
  }
}

FAQ

Should circuit breakers be async?

Circuit breaker logic (state checks) is synchronous. The wrapped function can be async. The circuit breaker class should support both.

How do I share circuit breaker state across servers?

Use a shared Redis store. Check the circuit state in Redis before allowing requests, and update failure counts atomically.

Can I use a circuit breaker for database calls?

Yes. Wrap database query functions with a circuit breaker to protect against database outages.

How do I test circuit breaker implementation?

Create a mock function that fails N times. Verify the circuit opens after N failures and rejects subsequent calls.

What is the overhead of a circuit breaker?

Minimal. State checks are O(1) integer comparisons. The circuit breaker adds less than 0.01ms per call.

Mini Project

Build a complete circuit breaker implementation with event emitter, registry, Express middleware, and health check endpoint.

const express = require("express");
const EventEmitter = require("events");

class CircuitBreaker extends EventEmitter {
  constructor(name, options = {}) {
    super();
    this.name = name;
    this.threshold = options.threshold || 5;
    this.resetTimeout = options.resetTimeout || 30000;
    this.state = "closed";
    this.failureCount = 0;
    this.successCount = 0;
    this.nextAttempt = Date.now();
  }

  async call(fn) {
    if (this.state === "open") {
      if (Date.now() < this.nextAttempt) {
        this.emit("rejected", { name: this.name });
        throw new Error("CircuitBreakerOpen");
      }
      this.changeState("half-open");
    }

    try {
      const result = await fn();
      this.onSuccess();
      return result;
    } catch (err) {
      this.onFailure(err);
      throw err;
    }
  }

  onSuccess() {
    this.failureCount = 0;
    this.successCount++;
    if (this.state === "half-open") {
      this.changeState("closed");
    }
  }

  onFailure(err) {
    this.failureCount++;
    this.successCount = 0;
    this.emit("failure", { name: this.name, count: this.failureCount, error: err.message });
    if (this.state === "half-open" || this.failureCount >= this.threshold) {
      this.changeState("open");
      this.nextAttempt = Date.now() + this.resetTimeout;
    }
  }

  changeState(state) {
    const prev = this.state;
    this.state = state;
    this.emit("stateChange", { name: this.name, from: prev, to: state });
  }

  getStatus() {
    return {
      name: this.name,
      state: this.state,
      failures: this.failureCount,
      threshold: this.threshold,
      nextRetry: new Date(this.nextAttempt).toISOString()
    };
  }
}

module.exports = CircuitBreaker;

What's Next

Now that you understand circuit breaker implementation, explore configuring failure thresholds. Then learn about configuring half-open probe behavior.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro