TrueSpec

API Rate Limiting Strategies

Token bucket, sliding window, Redis-backed, and per-user limits

What You’ll Build

After following this guide, you will have a working implementation of api rate limiting strategies in your project. Protect your API from abuse with rate limiting. Covers the token bucket algorithm (best for bursty traffic), sliding window (most accurate), Redis-backed limiting for distributed systems, and per-user/per-IP differentiation.

Use Cases & Problems Solved

  • Build reliable server endpoints that clients can consume consistently
  • Handle common backend patterns like routing, middleware, and error handling
  • Provide a clean interface between your frontend and data layer

Prerequisites

  • Node.js 18+
  • Redis for distributed rate limiting

Step-by-Step Implementation

Simple in-memory rate limiter

The following snippet shows how to simple in-memory rate limiter. Copy this into your project and adjust the values for your environment.

npm install express-rate-limit

Basic setup with per-route limits

The following snippet shows how to basic setup with per-route limits. Copy this into your project and adjust the values for your environment.

const rateLimit = require('express-rate-limit');

// Global limiter
app.use(rateLimit({ windowMs: 15 * 60 * 1000, max: 100 }));

// Strict limiter for auth endpoints
const authLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 5,
  message: { error: 'Too many login attempts, please try again in 15 minutes' },
  standardHeaders: true,
  legacyHeaders: false,
});
app.post('/auth/login', authLimiter, loginHandler);

Redis-backed for distributed systems

The following snippet shows how to redis-backed for distributed systems. Copy this into your project and adjust the values for your environment.

const { RedisStore } = require('rate-limit-redis');
const Redis = require('ioredis');

const redis = new Redis(process.env.REDIS_URL);

const apiLimiter = rateLimit({
  store: new RedisStore({ sendCommand: (...args) => redis.call(...args) }),
  windowMs: 60 * 1000,
  max: 60,
  keyGenerator: (req) => req.user?.id || req.ip, // per-user if authenticated
});

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

⚠️ Don’t Do This

❌ Using in-memory rate limiting in a multi-server deployment

// Each server has its own counter — user can hit each server separately!
app.use(rateLimit({ max: 100 })); // 3 servers = 300 effective requests!

✅ Use Redis-backed rate limiting for shared state across servers

app.use(rateLimit({
  store: new RedisStore({ sendCommand: (...args) => redis.call(...args) }),
  max: 100 // shared across ALL servers
}));

Testing

Add these tests to verify your API endpoints work correctly:

// __tests__/api.test.ts
import { describe, it, expect } from 'vitest';

describe('API Rate Limiting Strategies', () => {
  it('should return 200 for valid requests', async () => {
    const res = await fetch('/api/endpoint', { method: 'GET' });
    expect(res.status).toBe(200);
    const data = await res.json();
    expect(data).toBeDefined();
  });

  it('should return 400 for invalid input', async () => {
    const res = await fetch('/api/endpoint', {
      method: 'POST',
      body: JSON.stringify({}),
    });
    expect(res.status).toBe(400);
  });

  it('should handle errors gracefully', async () => {
    const res = await fetch('/api/endpoint/nonexistent');
    expect(res.status).toBe(404);
  });
});

Verification

# Test rate limiting:
for i in $(seq 1 10); do
  curl -s -o /dev/null -w '%{http_code}\
' http://localhost:3000/auth/login \
    -X POST -H 'Content-Type: application/json' -d '{"email":"test"}'
done
# First 5: 200/401, remaining: 429 Too Many Requests

Related Specs

Advanced

Secure Webhook Handler

Signature verification, idempotency, retry handling, and queue processing

API & Backend
Intermediate

Background Jobs with BullMQ

Queue setup, workers, retries, job scheduling, and monitoring dashboard

API & Backend
Intermediate

GraphQL API with Apollo Server

Schema, resolvers, context, dataloaders, and error handling

API & Backend