0x55aa
← Back to Blog

⚡ Circuit Breakers in Node.js: Stop the Cascade Before It Kills You

5 min read

⚡ Circuit Breakers in Node.js: Stop the Cascade Before It Kills You

Picture this: it's 2 AM, your phone is buzzing, and your entire backend is down. The culprit? A single third-party payment API that started responding slowly. Your app kept retrying, threads piled up, memory spiked, and now everything is broken — not just payments.

This, dear developer, is a cascading failure. And it's completely preventable.

Enter the circuit breaker pattern — one of the most underappreciated tools in the backend developer's toolkit.

What Even Is a Circuit Breaker?

Think about the circuit breaker in your home. When there's a dangerous power surge, it trips — cutting the circuit before your house burns down. It doesn't keep trying to push electricity through a broken wire. It stops, waits, then carefully lets you try again.

A software circuit breaker does the same thing. When a downstream service is struggling, instead of hammering it with requests (making things worse), you:

  1. Detect the failure (too many errors in a time window)
  2. Open the circuit — immediately fail requests without even trying
  3. Wait a bit (maybe 30 seconds)
  4. Half-open — let one test request through
  5. Close the circuit if it succeeds, or re-open if it fails

Your users get fast errors instead of hanging requests. Your broken dependency gets breathing room to recover. Everyone wins.

Three States, One Lifesaver

The circuit breaker lives in one of three states:

State What Happens
Closed Normal operation. Requests flow through. Errors are counted.
Open Circuit is tripped. Requests fail immediately (no network call).
Half-Open Cautious probe. One request is allowed through to test recovery.

The magic is in that Open state. Instead of waiting 30 seconds for a timeout on every request, you fail in milliseconds. Your request queue doesn't pile up. Your server stays healthy.

Let's Build One in Node.js

You could write a circuit breaker from scratch (great learning exercise), but in production you'll want something battle-tested. opossum is the go-to library for Node.js:

npm install opossum

Here's a real-world example wrapping an external API call:

const CircuitBreaker = require('opossum');
const axios = require('axios');

// The function we want to protect
async function fetchUserFromPaymentService(userId) {
  const response = await axios.get(
    `https://payments.example.com/users/${userId}`,
    { timeout: 3000 }
  );
  return response.data;
}

// Wrap it with a circuit breaker
const breaker = new CircuitBreaker(fetchUserFromPaymentService, {
  timeout: 3000,          // If it takes longer than 3s, it's a failure
  errorThresholdPercentage: 50,  // Open if 50%+ of requests fail
  resetTimeout: 30000,    // Try again after 30 seconds
  volumeThreshold: 5,     // Need at least 5 requests before evaluating
});

// Fallback when the circuit is open
breaker.fallback((userId) => {
  console.warn(`Circuit open for user ${userId}, using fallback`);
  return { id: userId, paymentStatus: 'unknown', cached: true };
});

// Optional: log state changes for observability
breaker.on('open', () => console.error('🔴 Circuit OPEN - payment service is down'));
breaker.on('halfOpen', () => console.warn('🟡 Circuit HALF-OPEN - testing payment service'));
breaker.on('close', () => console.info('🟢 Circuit CLOSED - payment service recovered'));

// Use it just like the original function
async function getUserPaymentInfo(userId) {
  return await breaker.fire(userId);
}

That's it. Your app now gracefully degrades instead of melting down. The fallback is crucial — it's what your users see when the circuit is open. Return cached data, a friendly "try again later" message, or a default state. Whatever makes sense for your domain.

Wiring It Into Express

In an Express app, this slots in cleanly:

const express = require('express');
const app = express();

app.get('/api/checkout/:orderId', async (req, res) => {
  try {
    const paymentInfo = await getUserPaymentInfo(req.params.orderId);
    
    if (paymentInfo.cached) {
      // Let the frontend know this is stale data
      res.set('X-Data-Source', 'fallback');
    }
    
    res.json(paymentInfo);
  } catch (err) {
    // Circuit is open AND no fallback defined, or fallback itself failed
    res.status(503).json({
      error: 'Payment service temporarily unavailable',
      retryAfter: 30,
    });
  }
});

Notice the 503 Service Unavailable with a retryAfter hint. Your frontend can display a "payments are temporarily down, please try again in 30 seconds" message instead of a spinning loader that never resolves. That's the difference between controlled degradation and chaos.

The Observability Angle

Circuit breakers are useless if you can't see what's happening. opossum ships with Prometheus-compatible stats out of the box:

const { prometheus } = require('opossum-prometheus');

// Expose breaker stats to your metrics endpoint
prometheus([breaker]);

app.get('/metrics', (req, res) => {
  res.set('Content-Type', prometheus.register.contentType);
  res.end(prometheus.register.metrics());
});

Now your Grafana dashboard can show you circuit state changes in real time. When the payment service goes flaky at 2 AM, you'll see the circuit open in your dashboard before your users start complaining. That's the good kind of 2 AM.

When to Use Circuit Breakers

Not everything needs one. Add circuit breakers when:

  • Calling external APIs (payment processors, SMS services, OAuth providers)
  • Hitting other internal microservices that have their own SLAs
  • Accessing slow database queries that could back up your connection pool
  • Any I/O operation where failure should be isolated, not propagated

You probably don't need one for:

  • Reads from a local in-memory cache
  • Pure computation functions
  • Operations that fail fast by design

The Big Takeaway

The circuit breaker pattern is about one thing: failing fast and recovering gracefully. It's the difference between a controlled service degradation and a full-blown outage. It respects your struggling dependencies instead of dogpiling on them. And it gives your users a meaningful response instead of a timeout.

Your home has circuit breakers in the fuse box. Your Node.js app deserves them too.


Have you implemented circuit breakers in your services? Drop a comment below — I'd love to hear what patterns you've used for fallbacks and whether you've caught any real cascades in the wild. And if this saved your 2 AM, share it with a teammate who's still one slow API away from disaster.