tutorial // Nov 18, 2022

How to Rate Limit Requests in Node.js Using Express Rate Limit

How to use the express-rate-limit package from NPM with Joystick global and route-level middleware to define rate limiting rules for your app.

How to Rate Limit Requests in Node.js Using Express Rate Limit

Getting started

For this tutorial, we're going to use CheatCode's full-stack JavaScript framework, Joystick. Joystick brings together a front-end UI framework with a Node.js back-end for building apps.

To begin, we'll want to install Joystick via NPM. Make sure you're using Node.js 16+ before installing to ensure compatibility (give this tutorial a read first if you need to learn how to install Node.js or run multiple versions on your computer):

Terminal

npm i -g @joystick.js/cli

This will install Joystick globally on your computer. Once installed, next, let's create a fresh project:

Terminal

joystick create app

After a few seconds, you will see a message logged out to cd into your new project and run joystick start. Before you run that, we need to install one more dependency express-rate-limit:

Terminal

cd app && npm i express-rate-limit

After this is installed, go ahead and start up your app:

Terminal

joystick start

After this, your app should be running and we're ready to get started.

Rate-limiting strategy

Before we dig into the code, it's worth discussing why this is important and how you can and should approach it in your own app.

Put simply: rate limiting is the process of limiting the number of requests that can be made in some time window by a single IP address. The point being to limit the potential for a DDoS (Distributed Denial of Service) attack: a fancy term for a single IP or user flooding a server with requests in an attempt to overwhelm and crash it. This term can also be used to describe behavior by spammers where the goal may not be to crash the site, but to maliciously execute some process rapidly (e.g., testing fraudulent credit card numbers).

Whatever the why, we want to stop this. As we'll see below, rate limiting is fairly simple to implement technically speaking. The real challenge, though, is deciding what our rate limit should be. There isn't an "industry standard" or "best practice" around this because it really depends on your app and how people use it.

A good rule of thumb is to ask: given the purpose of this route/URL, what is the reasonable number of requests I'd expect someone to make in X time frame? So, a more active user might make ~30 requests per minute while someone who's more invested in one thing might make 5-10 requests per minute.

A good place to start


The only way to know for certain what your ideal rate limit is is to measure activity and base it on that, however, assuming you're rate limiting a public-facing app, something like 1-5 requests per second is ideal. We say this number because a user might load a single page that itself makes 5 requests very rapidly (again, this depends on your app and its implementation). A floor of 1 request per second for a content-based site like CheatCode is ideal because the number of requests we make per page is low (typically just a single request to the server to get the content for the page).

Though it may be frustrating, the best advice is to experiment. What's cool is that rate limiters work in development so you can simulate normal user behavior with a rate limiter in place and see if it causes any frustration. Similarly, you can set up a "fake DDoS" using a loop in your browser console and see if a more aggressive or lenient rate limit is necessary.

Now, let's see how to actually implement a rate limit. We're going to look at two different types: a global rate limit that can apply to all routes and route-specific rate limits that only apply to a single route.

Adding a global rate limiter

To start, let's look at how to add a global rate limit. To do it, we're going to open up the /index.server.js file at the root of the project we just set up and add a middleware property set to an array on the object passed to node.app() there:

/index.server.js

import node from "@joystick.js/node";
import api from "./api";

node.app({
  api,
  middleware: [],
  routes: {
    "/": (req, res) => {
      res.render("ui/pages/index/index.js", {
        layout: "ui/layouts/app/index.js",
      });
    },
    "*": (req, res) => {
      res.render("ui/pages/error/index.js", {
        layout: "ui/layouts/app/index.js",
        props: {
          statusCode: 404,
        },
      });
    },
  },
});

This middleware array is supported by the @joystick.js/node package we're importing the node.app() function from that we're calling here. This starts up an Express.js HTTP server for us and automatically defines the api and routes for our app.

Here, middleware represents an array of Express.js middleware functions that we want to perform on all requests before handing those requests off to our routes (quite literally, these functions are the "software" that sit in the middle of an inbound request and our app's routes).

/index.server.js

import node from "@joystick.js/node";
import rateLimit from 'express-rate-limit';
import api from "./api";

node.app({
  api,
  middleware: [
    rateLimit({
      windowMs: 60 * 1000, // 60 seconds.
      max: 1,
      message: '<p style="color: red;">Slow down there cowboy. Too many requests. Please wait and try again later.</p>',
    }),
  ],
  routes: {
    "/": (req, res) => {
      res.render("ui/pages/index/index.js", {
        layout: "ui/layouts/app/index.js",
      });
    },
    "*": (req, res) => {
      res.render("ui/pages/error/index.js", {
        layout: "ui/layouts/app/index.js",
        props: {
          statusCode: 404,
        },
      });
    },
  },
});

To add our global middleware, here, we've imported the rateLimit function from the express-rate-limit package we installed earlier. To put it to use, we add a call to that function in the middleware array we added to our node.app() options object. When calling the function, we pass the settings for the rate limit we want to apply:

  • windowMs describes the window of time in milliseconds at which our max should be applied. In other words: "within this many milliseconds" with the limit resetting at this interval (e.g., reset the count to 0 every 60 seconds).
  • max the maximum number of requests to allow for an IP address/user per the windowMs. Here, we've set this to a silly 1 per minute to showcase the behavior.
  • message the message that will be returned to the request once the rate limit is reached. This can be a plain string of text or HTML like we've shown here.

That's it. Now when we visit our app, we'll only be able to load a route once per minute. If we try a second request, we'll see our message displayed in the browser signifying that our rate limit kicked in.

Obviously we don't want to limit our app to be only accessible once per minute so like we hinted at above, it's wise to adjust this to something like the following as a starting point:

/index.server.js

rateLimit({
  windowMs: 1000, // 1 second.
  max: 3,
  message: '<p style="color: red;">Slow down there cowboy. Too many requests. Please wait and try again later.</p>',
})

Again, this is a best guess. Depending on the number of requests your app does to the server on a single page load, you will want to adjust the max number here.

Adding route-specific rate limiters

While having a global rate limit is nice, where rate limiting really shines is on a per-route basis. This means that we can have one route without any rate limiting while another has something more aggressive in place. The good news: the code is nearly identical, we just need to tweak how we define our routes:

/index.server.js

import node from "@joystick.js/node";
import rateLimit from 'express-rate-limit';
import api from "./api";

node.app({
  api,
  middleware: [
    rateLimit({
      windowMs: 1000, // 1 second.
      max: 3,
      message: '<p style="color: red;">Slow down there cowboy. Too many requests. Please wait and try again later.</p>',
    }),
  ],
  routes: {
    "/": (req = {}, res = {}) => {
      res.render("ui/pages/index/index.js", {
        layout: "ui/layouts/app/index.js",
      });
    },
    "/test": {
      method: 'GET',
      middleware: [
        rateLimit({
          windowMs: 10 * 1000, // 10 seconds.
          max: 5,
        }),
      ],
      handler: (req, res) => {
        res.render("ui/pages/index/index.js", {
          layout: "ui/layouts/app/index.js",
        });
      },
    },
    "*": (req, res) => {
      res.render("ui/pages/error/index.js", {
        layout: "ui/layouts/app/index.js",
        props: {
          statusCode: 404,
        },
      });
    },
  },
});

Above, we've added an additional route /test which is defined a bit differently from the / route that was defined for us when we created our app with joystick create app above.

Here, we use the "custom" or object-based approach to defining a route in a Joystick app which allows us to specify things like which HTTP method(s) a route supports and, here, any custom middleware we want to run for that route alone.

Just like we saw with our global middleware up on our node.app() options object, here, we're adding a call to the rateLimit() function and passing that call to our middleware array. The difference is that the rules defined here will only apply to our /test/ route (as an example, we're doing a max of 5 requests per minute). All other routes will be left as-is and handled by the global rate limiter.

To differentiate, here, notice that we've left off the custom HTML message allowing the default message to display so we can differentiate between the two.

Wrapping up

In this tutorial, we learned how to wire up a rate limiter for our app using the express-rate-limiter package. We learned why rate limits are important, defined some guidelines for how to leverage them, and then how to put them to practice. First we learned how to define a global rate limiter that applies to all of our routes and then, a local route-specific rate limiter that applies to a single route.

Written By
Ryan Glover

Ryan Glover

CEO/CTO @ CheatCode