loading...
Cover image for Node.js Resiliency Concepts: The Circuit Breaker
AppSignal

Node.js Resiliency Concepts: The Circuit Breaker

gasparandr profile image Andrei Gaspar Originally published at blog.appsignal.com ・16 min read

In 2009 Node.js opened up a door for front-end developers to dip their toes into the world of servers
without having to leave the comfort of their language.

It's almost effortless to get started with Node.
You can basically copy-paste an entire HTTP server into existence and then install an ODM and you've got your CRUD app ready to roll!

However, if we've learned anything from the amazing Spider-Man,
it's that with great power, comes great responsibility.

So, in this article, we're going to discuss how you can wield your Node-given powers responsibly,
and design servers that don't just work, but are also resilient and adaptive to failures.

👋 AppSignal now supports Node.js as well. In the coming weeks, we’ll be adding a whole array of integrations i.e. support more things out-of-the-box. We already have support for Next.js, PostgreSQL, and Express. Learn more about AppSignal for Node.js.

Resiliency and Chill

One of the biggest names in the industry when it comes to server resiliency design is Netflix.
They are extremely dedicated to designing robust systems that will serve us all seasons
of Grey's Anatomy any minute of the day!

But what is this "resiliency" anyway?

Well, resiliency is just a fancy word for the ability of your system to recover from failures
and continue operating.

If the power goes out and it continues to work, your system is resilient.
If there is an equipment failure and the system keeps on going, it is even more resilient.
If you hit it with a baseball bat and the system is still up... you get the idea.

However, in our case, we're more interested in providing API resiliency.
So, let's see how we would identify a resilient API.
What are some of the core principles of a resilient API?

Well, let's learn from the pros. Let's see what Netflix has to say about it.

Netflix defines the principles of resiliency as follows:

  • A failure in a service dependency should not break the user experience.
  • The API should automatically take corrective action when one of its service dependencies fails.
  • The API should be able to show us what's happening right now, in addition to what was happening 15-30 minutes ago, yesterday, last week, etc.

They are also responsible for fault tolerance libraries and sophisticated tools
for dealing with latency and fault tolerance in distributed systems.

To deal with the problem of fault tolerance, most of these solutions use
a popular software design pattern called circuit-breaker,
which is the exact pattern that we're going to be discussing in detail in the upcoming sections.

The Circuit Breaker Pattern

The Circuit Breaker in software design is named after it's equivalent in electrical engineering,
where it serves as a switch designed to stop the flow of the current in an electric circuit.
It is used as a safety measure to protect the circuit from overload or short circuit.

Circuit breakers come in all shapes and sizes, there are some that reset automatically,
some that need to be reset manually, but they all essentially do the same thing — open the circuit
if there's trouble.

The Circuit Breaker was popularized by Miachel Nygard with his book Release It!,
where he describes this pattern along with other useful information about architecting
resilient and performant software.

So if the electrical circuit breaker manages the flow of current, what does it's software equivalent do?

The circuit breaker manages the flow of requests to an upstream resource.

Let's think of the upstream resource as a remote server for the time being,
but it is certainly not limited to being that. Circuit breakers can also be used
locally to protect one part of your system from failure from another part.

The circuit breaker monitors for failures, and when the failures reach a certain threshold,
it trips and none of the successive calls will be forwarded to the upstream resource.

Why Would We Bother Using a Circuit Breaker?

With the rising popularity of microservices, it is common for apps to make remote calls
to other apps running on different processes across a network.
It is often the case that the system is spread out across multiple machines as well.

Some of these services act as dependencies for others,
and it is not unusual to have multiple dependencies upstream.

Even if we forget about microservices altogether,
think about how common it is for applications to make remote calls.
It is almost unavoidable that it will have integrations and will rely on upstream resources.

Another popular case is an API gateway, where a service's primary purpose is to proxy requests upstream.
In this case, the health of the application is very closely tied to the health of the upstream resource.

So, we have all these cases where requests are being passed upstream, but why use a circuit breaker?
And why don't we just let the request fail at its own pace?

Preserve Resources

Wasteful calls pile up on the upstream resource which might be already struggling
with serving previous requests, further escalating the problem.

Wasteful calls can also be a big problem for the service making those calls.

Resources such as threads might be consumed while waiting for the upstream resource to respond,
which can lead to resource exhaustion.

This can in turn lead to the service being unable to handle other requests.

So, wasteful calls can bring down services,
and the failure can cascade to other services throughout the application.

Fail Fast

Imagine you're throwing a party on a Saturday evening.
You're making preparations, sending invitations to all your friends.

Would you prefer them to respond instantly, or would you prefer them to respond the day after the party?

I know, I'd go with option one.

We want responses fast so that we can adapt to them even if it means not getting what we asked for.

This concept in systems design is called failing fast.

Fail Proactively

When upstream resources give us lemons, we make lemonade.

You might not be able to prevent upstream failures, but you can always manage them proactively,
and make the most out of what you got.

Here are some common solutions to improve the failure:

  • Fallbacks - in certain cases, you might be able to fall back to another service.
  • Defaults - in certain cases, the integrity of the data is not crucially important, and defaults serve a good enough purpose until the upstream resource recovers.
  • Cache - you can serve cached requests until the upstream resource recovers.

Avoid Polluting the Logs

Your monitoring solution is one of the most important components of your system.
Without it, you're completely blind to what happens inside the dark realm of containers and Linux servers.

Metrics and logs are your eyes and ears.
And the better the quality of logs, the better you're able to understand what happens with your system.

If requests keep failing and you don't have a system in place that handles the situation gracefully,
it will end up pumping ungodly amounts of pollution into your logs.

Circuit Breaker States

The circuit breaker has 3 main states which give us a clue about the
health of the upstream resource or endpoint that we're targeting.

  • Closed - the closed state means that the circuit is closed and everything is running smoothly. Just like in the case of an electrical circuit.
  • Open - this state means that there is currently no connection upstream. In the case of an electrical circuit, if it is open, electricity cannot make its way through it.
  • Half Open - the half open state means it has experienced difficulties reaching the upstream resource, but it's now testing the waters with new requests to see if it can stabilize. If it does, it goes to the closed state, if requests fail, it opens the circuit again.

Even though these are the conventional names of circuit breaker states,
I prefer not to use them because I find them deceptive and can be misleading for developers.

When people see Open they're intuitively associating it with OK,
and Closed sounds a lot like something went wrong.

What I prefer to use instead are colors e.g. Red, Yellow, Green or
descriptive names like Failing, Stabilizing, OK.

So, for this demonstration, we're going to use colors to describe states,
but remember, this is just personal preference!

Creating Your Own Circuit Breaker

There are plenty of libraries out there that we could use to implement our circuit breaker,
but that would beat the purpose of the article since our goal is to understand how
the circuit breaker pattern is implemented.

So let's reinvent the wheel to learn how the wheel works.

What we are going to code:

  • The simplest Express.js server to act as our upstream resource and simulate succeeding and failing requests.
  • A configurable Circuit Breaker class that uses the Axios library to make requests, and has basic logging capability.
  • A few lines of code where we make use of our Circuit Breaker.

We are going to use TypeScript to implement these features.

So, let's dive in!

The first thing we want to do is to navigate to an empty directory of our choice,
which will be our work directory, and execute the npm init command.

npm init -y

Once we have the package.json file, it's time to install our main dependencies.

npm install --save express axios

Since we're using TypeScript, we'll also need some dev dependencies, so let's install those as well.

npm install --save-dev typescript @types/express @types/axios

Next, we're going to need a tsconfig.json file to hold our TypeScript configuration.
You can use the one below.

{
  "compilerOptions": {
    "outDir": "./build",
    "lib": [ "es5", "es6" ],
    "module": "commonjs",
    "target": "es6",
    "sourceMap": true
  },
  "exclude": [
    "node_modules"
  ]
}

Great, now our work directory should contain a node_modules directory
and three files: package.json, package-lock.json, and tsconfig.json.

It is time to copy-paste a basic Express server into existence.

Create a file called index.ts and paste the following lines of code into it.

// index.ts

import {Request, Response} from "express";

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

const port = 3000;


app.get( '/', (req: Request, res: Response) => {

    if ( Math.random() > 0.5 ) {
        res.status( 200 ).send( "Success!" );
    } else {
        res.status( 400 ).send( "Failed!" );
    }

});

app.listen( port, () => console.log( `Listening at http://localhost:${ port }` ) );

The above code snippet summons a simple express server that will be listening to GET requests on
localhost:3000, and randomly failing with status 400 or responding with status 200.
We'll be able to use this endpoint to test our Circuit Breaker.

Before we go further with the implementation, let's add a couple of convenience scripts to
our package.json file so that we can build and start the server using npm commands.

In the scripts section of your package.json, copy and paste the following:

...
  "scripts": {
    "build": "tsc",
    "start-server": "npm run build && node build/index.js"
  },
 ...

This will allow you to start your server with a simple npm command.

npm run start-server

Once the command is executed, the server should print "Listening at.. localhost:3000" to the console.

So far so good! Let's move on to the meat of the article, which is the Circuit Breaker itself!

Let's create a circuit-breaker directory, which will contain all the assets related to the Circuit Breaker.

mkdir circuit-breaker

Now, let's navigate into this directory and start thinking about the components
that we'll need to make the circuit breaker a reality.

First, we talked about states, so let's create a file called BreakerStates.ts to define our states.

We're going to use an enum and color codes for the states, to make it a bit more developer-friendly.

In the BreakerStates.ts file let's declare an enum like so:

// circuit-breaker/BreakerStates.ts

export enum BreakerState {
    GREEN = "GREEN",
    RED = "RED",
    YELLOW = "YELLOW"
}

Great, now that we have the states, what else do we need?

We'll need some configuration options for our Circuit Breaker
that will answer the following questions for us:

  • How many failures do we allow before moving to RED state? Let's call this our failureThreshold
  • How many successes do we need before moving to GREEN state? Let's call this our successThreshold
  • Once we are in RED state, how much time should we wait before we allow a request to pass through? We'll call this our timeout.

So, immediately, we can see that we'll need a public class named BreakerOptions
that can hold these properties. We could also opt for an interface trick here, but let's stick
to the conventional class-based approach.

Let's create a file called BreakerOptions.ts and define our public class.

// circuit-breaker/BreakerOptions.ts

export class BreakerOptions { constructor(
    public failureThreshold: number,
    public successThreshold: number,
    public timeout: number
){}}

Once we have the States and Options defined, we can start planning the CircuitBreaker
class implementation. Since the circuit breaker will be making requests,
and we're using Axios as our HTTP library, we'll have Axios as our dependency for this class.

Let's think about the properties that we'll have in the class.

  • request - the request property will contain details about the request that we are going to attempt. Since we integrated with Axios, it would be smart to have this as the Axios request configuration. We can use the AxiosRequestConfig type for that.
  • state - this property can hold our circuit breaker state. We have a BreakerState type created for this.
  • failureCount - we will need something to count the number of failures with, let's use this property for that purpose.
  • successCount - same as failureCount, but for tracking successes.
  • nextAttempt - we will need a property to store a timestamp for the next time when we try a request when we're in the RED state.

Let's not forget about the BreakerOptions we defined!
We'll need to store those inside the class as well.
It would also be smart to make them optional and have default values defined for them within the class.

  • failureThreshold - lets us know when to switch to RED state.
  • successThreshold - lets us know when to switch to GREEN state.
  • timeout - lets us know how much to wait before the next attempt (in milliseconds).

That is a handful of properties to be defined.
So let's set all this up before we move to the logic implementation.

Let's create a file called CircuitBreaker.ts where we'll define our CircuitBreaker class.

// circuit-breaker/CircuitBreaker.ts

import { BreakerOptions } from "./BreakerOptions";
import { BreakerState } from "./BreakerStates";
import { AxiosRequestConfig } from "axios";

const axios = require("axios");


class CircuitBreaker {
    private request: AxiosRequestConfig;
    private state: BreakerState;

    private failureCount: number;
    private successCount: number;

    private nextAttempt: number;

    // Options
    private failureThreshold: number;
    private successThreshold: number;
    private timeout: number;


    constructor(request: AxiosRequestConfig, options?: BreakerOptions) {

        this.request        = request;
        this.state          = BreakerState.GREEN;

        this.failureCount   = 0;
        this.successCount   = 0;
        this.nextAttempt    = Date.now();

        if ( options ) {
            this.failureThreshold   = options.failureThreshold;
            this.successThreshold   = options.successThreshold;
            this.timeout            = options.timeout;
        } else {
            // Define defaults
            this.failureThreshold   = 3;
            this.successThreshold   = 2;
            this.timeout            = 3500;
        }
    }

}

Now it's time to think about the methods that we'll need.
Let's plan them out and then we can start implementing them one by one.

  • log - we'll need a method to log the current state of the Circuit Breaker. We'll be able to use this same method to integrate with our monitoring system as well.
  • exec - the execute method will be a public API through which we'll be able to trigger the request attempt. We'll need to make this into an asynchronous function because we'll be waiting for a server response.
  • success - this method will handle the successful executions and return the upstream response.
  • failure - this method will handle the failed attempts and return the upstream response.

So let's start at the beginning and define our log method as such:

// circuit-breaker/CircuitBreaker.ts

   private log(result: string): void {

        console.table({
            Result: result,
            Timestamp: Date.now(),
            Successes: this.successCount,
            Failures: this.failureCount,
            State: this.state
        });
    }

All it's responsible for is taking the result and displaying it in a nice tabular format,
including other details about the current state of our Circuit Breaker.

Let's move on to the success method and define some logic. Here's what it should do for us.

  • Return the successful response.
  • Reset the failure count.
  • Log the status so that we're aware of what happened.
  • If in YELLOW state, increment the success count — and if the success count is larger than the threshold defined, reset and move to GREEN state.

Sounds easy enough, let's write the code!

// circuit-breaker/CircuitBreaker.ts

    private success(res: any): any {

        this.failureCount = 0;

        if ( this.state === BreakerState.YELLOW ) {
            this.successCount++;

            if ( this.successCount > this.successThreshold ) {
                this.successCount = 0;
                this.state = BreakerState.GREEN;
            }
        }

        this.log( "Success" );

        return res;

    }

Great, we have success down — we'll do the same for failure. Here's the gist of it.

  • Return the response.
  • Increment the failure count.
  • Log the status so that we're aware of the failure.
  • If the failure count exceeds the threshold, move to RED state, and define when our next attempt should take place.

Here's the code:

// circuit-breaker/CircuitBreaker.ts

    private failure(res: any): any {

        this.failureCount++;

        if ( this.failureCount >= this.failureThreshold ) {
            this.state = BreakerState.RED;

            this.nextAttempt = Date.now() + this.timeout;
        }

        this.log( "Failure" );

        return res;
    }

And finally, the most important method to define, the exec method!
This stands at the core of our mechanism. Let's see what it should do for us.

  • Most importantly, if the state is RED and the next attempt is scheduled sometime in the future, throw an Error and abort. We do not allow the request to go upstream.
  • If the state is RED but the timeout period expired, we want to switch state to YELLOW and allow the request to pass.
  • If the state is NOT RED we try to make the request, and based on whether the request succeeded or failed, we call the appropriate handler method.

Simple enough, right? Let's see how the implementation looks.

// circuit-breaker/CircuitBreaker.ts

    public async exec(): Promise<void> {

        if ( this.state === BreakerState.RED ) {

            if ( this.nextAttempt <= Date.now() ) {
                this.state = BreakerState.YELLOW;
            } else {
                throw new Error( "Circuit suspended. You shall not pass." );
            }
        }

        try {
            const response = await axios( this.request );

            if ( response.status === 200 ) {
                return this.success( response.data );
            } else {
                return this.failure( response.data );
            }
        } catch ( err ) {
            return this.failure( err.message );
        }
    }

So, now that we have our CircuitBreaker class all set up,
it's time to see how we can use it to perform requests.

Before anything else though, here's the complete implementation of the class,
you can review it to see if it matches yours!

// circuit-breaker/CircuitBreaker.ts

import { BreakerOptions } from "./BreakerOptions";
import { BreakerState } from "./BreakerStates";
import { AxiosRequestConfig } from "axios";

const axios = require("axios");



export class CircuitBreaker {
    private request: AxiosRequestConfig;
    private state: BreakerState;

    private failureCount: number;
    private successCount: number;

    private nextAttempt: number;

    // Options
    private failureThreshold: number;
    private successThreshold: number;
    private timeout: number;



    constructor(request: AxiosRequestConfig, options?: BreakerOptions) {

        this.request        = request;
        this.state          = BreakerState.GREEN;

        this.failureCount   = 0;
        this.successCount   = 0;
        this.nextAttempt    = Date.now();

        if ( options ) {
            this.failureThreshold   = options.failureThreshold;
            this.successThreshold   = options.successThreshold;
            this.timeout            = options.timeout;
        } else {
            // Define defaults
            this.failureThreshold   = 3;
            this.successThreshold   = 2;
            this.timeout            = 3500;
        }
    }



    private log(result: string): void {

        console.table({
            Result: result,
            Timestamp: Date.now(),
            Successes: this.successCount,
            Failures: this.failureCount,
            State: this.state
        });
    }



    public async exec(): Promise<void> {

        if ( this.state === BreakerState.RED ) {

            if ( this.nextAttempt <= Date.now() ) {
                this.state = BreakerState.YELLOW;
            } else {
                throw new Error( "Circuit suspended. You shall not pass." );
            }
        }

        try {
            const response = await axios( this.request );

            if ( response.status === 200 ) {
                return this.success( response.data );
            } else {
                return this.failure( response.data );
            }
        } catch ( err ) {
            return this.failure( err.message );
        }
    }



    private success(res: any): any {

        this.failureCount = 0;

        if ( this.state === BreakerState.YELLOW ) {
            this.successCount++;

            if ( this.successCount > this.successThreshold ) {
                this.successCount = 0;
                this.state = BreakerState.GREEN;
            }
        }

        this.log( "Success" );

        return res;

    }



    private failure(res: any): any {

        this.failureCount++;

        if ( this.failureCount >= this.failureThreshold ) {
            this.state = BreakerState.RED;

            this.nextAttempt = Date.now() + this.timeout;
        }

        this.log( "Failure" );

        return res;
    }

}

Looking good? Great!

Alongside our index.ts file, we can create a test.ts file as well, that will contain a
couple of lines of code for testing our masterpiece.

// test.ts
import { CircuitBreaker } from "./circuit-breaker/CircuitBreaker";



const circuitBreaker = new CircuitBreaker({
    method: "get",
    url: "http://localhost:3000"
});


setInterval(() => {
    circuitBreaker
        .exec()
        .then( console.log )
        .catch( console.error )
}, 1000 );

In the code above, we imported the CircuitBreaker,
created an instance of it and started calling the exec() method at an interval of 1 second.

Let's add one more script to our package.json file to be able to run this test conveniently.

The scripts section should look like this, updated with the test-breaker script:

...
  "scripts": {
    "build": "tsc",
    "start-server": "npm run build && node build/index.js",
    "test-breaker": "npm run build && node build/test.js"
  },
  ...

Now, let's make sure the server is running!

npm run start-server

And in a separate terminal window, let's run the circuit breaker test as well.

npm run test-breaker

Once executed, here's an example of the log stream that you should be seeing in your terminal.

Success!
┌───────────┬───────────────┐
│  (index)  │    Values     │
├───────────┼───────────────┤
│  Result   │   'Failure'   │
│ Timestamp │ 1592222319902 │
│ Successes │       0       │
│ Failures  │       1       │
│   State   │    'GREEN'    │
└───────────┴───────────────┘
Request failed with status code 400
┌───────────┬───────────────┐
│  (index)  │    Values     │
├───────────┼───────────────┤
│  Result   │   'Failure'   │
│ Timestamp │ 1592222320906 │
│ Successes │       0       │
│ Failures  │       2       │
│   State   │    'GREEN'    │
└───────────┴───────────────┘
..............
┌───────────┬───────────────┐
│  (index)  │    Values     │
├───────────┼───────────────┤
│  Result   │   'Failure'   │
│ Timestamp │ 1592222321904 │
│ Successes │       0       │
│ Failures  │       3       │
│   State   │     'RED'     │
└───────────┴───────────────┘
...............
┌───────────┬───────────────┐
│  (index)  │    Values     │
├───────────┼───────────────┤
│  Result   │   'Failure'   │
│ Timestamp │ 1592222331941 │
│ Successes │       2       │
│ Failures  │       1       │
│   State   │   'YELLOW'    │
└───────────┴───────────────┘
...............

From this point forward, you can have as much fun with it as you like.

You can start and stop the server while the circuit breaker is running to notice what happens,
and you can also create different breakers with different BreakerOptions like so:

// test.ts

import { CircuitBreaker } from "./circuit-breaker/CircuitBreaker";
import { BreakerOptions } from "./circuit-breaker/BreakerOptions";



const breaker1 = new CircuitBreaker({
    method: "get",
    url: "http://localhost:3000"
}, new BreakerOptions( 3, 5, 5000 ) );


const breaker2 = new CircuitBreaker({
    method: "get",
    url: "http://localhost:3000"
}, new BreakerOptions( 6, 7, 1000 ) );


setInterval(() => {
    breaker1
        .exec()
        .then( console.log )
        .catch( console.error )
}, 500 );

setInterval(() => {
    breaker2
        .exec()
        .then( console.log )
        .catch( console.error )
}, 1500 );

Implementation Granularity

Once you have it up and running, the design choices are in your hands.
You can choose to make a circuit breaker responsible for an entire upstream service or just target individual endpoints depending on your needs.

Feel free to use different HTTP integrations, experiment with extending the
breaker options and define multiple endpoints in your server to test with.

Here are additional feature ideas to consider:

  • Create an API for the breaker so that it can be reset or tripped by the operations staff.
  • Implement an event system around the Circuit Breaker so you can subscribe different parts of your application to it.
  • Integrate the breaker with your favorite monitoring solution.
  • Implement a Queue to automatically retry failed requests. (Warning: don't use this for requests downstream waiting for a response.)
  • Implement Caching to serve failed requests from the cache.

Parting Words

This sums up our overview of the Circuit Breaker pattern!
I hope this article helped you grasp a few resiliency principles
and it sparked your imagination to try extending this boilerplate with some creative solutions.

We reinvented the wheel to understand how it works,
but custom solutions are not always the best choice.
You have to analyze complexity and keep maintenance overhead in sight.

Once you're comfortable with the basics, I would suggest you check out a few npm packages
which are designed specifically for this purpose.
There are a couple of candidates out there like opossum, hystrixJS, and brakes.

It all depends on your requirements and I trust you to make the right decisions
in your journey to improve system resiliency!

P.S. If you liked this post, subscribe to our new JavaScript Sorcery list for a monthly deep dive into more magical JavaScript tips and tricks.

P.P.S. If you'd love an all-in-one APM for Node or you're already familiar with AppSignal, go and check out AppSignal for Node.js.

Daydreaming about APIs and imagining web services — our guest author Andrei is a solutions architect by day and the co-founder of Boardme by night. When he's not typing frantically in a terminal, he's exploring nature, pretends to draw, and supplies bystanders with unsolicited gym advice.

Posted on by:

gasparandr profile

Andrei Gaspar

@gasparandr

Technical founder at boardme.io | tech contributor at uidesigndaily.com

AppSignal

AppSignal is made for teams that want to build high quality applications. It offers amazing insights into errors and performance issues, plus host monitoring and an easy to use custom metrics platform

Discussion

markdown guide