Build a Custom API Rate Limiter in Node.js From Scratch

Customized Rate Limiting in Node.js controls the number of requests users send to your server within a certain window of time. This helps protect your API from spam, abuse, and sudden spikes in traffic that might slow down or even crash your app. This guide will walk you through building a custom API rate limiter in Node.js from scratch, so you understand how everything works under the hood.

Instead of using third-party plugins, you will see how node js rate limiting without library dependencies gives you full control, allowing you to tune the logic, error messages, and storage layer to your product and business rules.

Why Build a Custom Rate Limiter in Node.js?

Using a library is faster, but building an API rate limiter yourself in Node.js teaches you how the core logic works. If you write the code yourself, you will understand what happens with each request and why a client gets blocked or even customize it as per your requirements.

With a custom rate limiter, you have more options like use user IDs instead of IPs, apply different limits per plan, or change the window size on the fly. You can also plug in different backends for storage, like Redis or a database, without having to wait for a plugin update.

For some teams, security or compliance rules don’t allow having too many external dependencies. In Express JS, creating your own rate limiting middleware reduces your dependency footprint and makes audits easier. You can debug your own logic when issues appear in production, instead of digging into an unknown package.

Common Use Cases for Rate Limiting

Practical scenarios where you want to restrict API calls with Node.js custom code are as follows:

  • Limit login attempts to slow down password guessing and brute force attacks
  • Limit the abusive usage of one client against free-tier API endpoints
  • Throttle expensive search/report endpoints that hit the database heavily
  • Control traffic from webhooks or third-party integrations that can misbehave
  • Protect routes for checkout, payment, or order creation against spam and bots
  • Protect internal microservice endpoints so that one service cannot overload another

Building Custom Rate Limiting Middleware

Let’s create rate limiter Node JS from scratch using a simple in-memory store. This solution works well for small apps, prototypes, or services running on a single instance.

Here, we will create a function that returns Express middleware. The middleware tracks how many requests an IP sends in a given time window. When the user crosses the allowed limit, the middleware blocks the request with a 429 status code.

const express = require('express');

const app = express();

function createRateLimiter(options) {
  const windowMs = options.windowMs;
  const maxRequests = options.maxRequests;
  const hits = new Map();

  return function rateLimiter(req, res, next) {
    const key = req.ip;
    const now = Date.now();
    const record = hits.get(key) || { count: 0, startTime: now };

    if (now - record.startTime > windowMs) {
      record.count = 1;
      record.startTime = now;
    } else {
      record.count += 1;
    }

    hits.set(key, record);

    if (record.count > maxRequests) {
      res.status(429).json({
        status: 429,
        message: 'Too many requests from this IP. Please try again later.'
      });
      return;
    }

    next();
  };
}

const limiter = createRateLimiter({
  windowMs: 15 * 60 * 1000,
  maxRequests: 100
});

app.use(limiter);

app.get('/', (req, res) => {
  res.send('Hello from a rate limited API');
});

app.listen(3000);

Above code is how this custom API rate limiter implementation in Node.js behaves in real life. Imagine a public endpoint that shows product listings. Without protection, a script could hit it thousands of times per minute and slow down the rest of your users or even scrap your data programatically. With this middleware, each IP gets only 100 requests every 15 minutes. After that, the API responds with 429 until the window resets.

You can utilize this rate limit for each endpoint as per your requirement with following similar approach like below example.

const regularLimiter = createRateLimiter({
  windowMs: 15 * 60 * 1000,
  maxRequests: 100
});

const authLimiter = createRateLimiter({
  windowMs: 60 * 60 * 1000,
  maxRequests: 5
});

app.get('/', regularLimiter, (req, res) => {
  res.send('Hello from a rate limited API');
});

app.post('/auth/login', authLimiter, (req, res) => {
  res.send('Login endpoint with strict rate limit');
});

Here it will limit 100 request for regular endpoints. However, for auth API endpoints it will only allow 5 requests per hour.

Extending Further: Store Counts in Redis for Scaling

The in-memory solution works for a single server or smaller application, but it breaks when you run multiple instances behind a load balancer. Each instance would keep its own hit counter, so one client could bypass the limit by hitting different servers.

To support a distributed setup or extending it further, you can move the counters to a central store like Redis. Redis runs in memory and handles fast read and write operations. Let’s modify rate limitor to store request count per IP and automatically expire those keys after time.

const express = require('express');
const { createClient } = require('redis');

const app = express();

const redis = createClient();

async function initRedis() {
  await redis.connect();
}

function createRedisRateLimiter(options) {
  const windowMs = options.windowMs;
  const maxRequests = options.maxRequests;
  const windowSeconds = Math.floor(windowMs / 1000);

  return async function rateLimiter(req, res, next) {
    try {
      const key = `rate:${req.ip}`;
      const current = await redis.incr(key);

      if (current === 1) {
        await redis.expire(key, windowSeconds);
      }

      if (current > maxRequests) {
        res.status(429).json({
          status: 429,
          message: 'Too many requests. Please slow down.'
        });
        return;
      }

      next();
    } catch (error) {
      next();
    }
  };
}

const apiLimiter = createRedisRateLimiter({
  windowMs: 60 * 1000,
  maxRequests: 20
});

app.use('/api', apiLimiter);

app.get('/api/data', (req, res) => {
  res.json({ data: 'Some protected data' });
});

initRedis().then(() => {
  app.listen(3001);
});

It helps to apply rate limiting middleware in Express JS that works across multiple instances. Each time a client sends a request, all instances talk to the same Redis server. The first request creates the key with a counter of 1 and a time-to-live equal to the window duration. Every new request increases the counter until it goes above maxRequests.

Conclusion

Custom Rate limiting in Node.js is one of the simplest and most powerful tools that can protect your APIs. By putting a restriction on API calls with Node.js custom code, you retain control over your algorithm, storage, and user experience. You also reduce your reliance on external dependencies. In this guide, you saw how to build a custom in-memory rate limiter and how to extend it with Redis for distributed setups.

If you prefer a ready-to-use solution instead of custom code. Here is ou guide on Rate Limiting in Node.js using the express-rate-limit package.