Skip to content

Rate Limiting Project — Build a Complete Rate Limiting System

DodaTech Updated 2026-06-28 5 min read

In this tutorial, you will learn about Rate Limiting Project. We cover key concepts, practical examples, and best practices to help you master this topic.

This rate limiting project guides you through building a production-ready rate limiting system with multiple algorithms, tiered limits, Redis persistence, security protection, and comprehensive headers.

What You'll Learn

By completing this project, you will integrate all rate limiting concepts into a single system, handling real-world scenarios like multi-server deployment and abuse detection.

Why It Matters

Individual rate limiting tutorials teach isolated concepts. This project shows how they compose into a complete system, just like DodaTech's production API protection.

Real-World Use

The system mirrors DodaTech's API rate limiting: per-IP global limits, per-user authenticated limits, strict login protection, and graduated penalties for repeat offenders.

Project Learning Path

flowchart LR
  A[Security Limiting] --> B[Rate Limiting Project]
  B --> C[Complete System]
  C --> D[Production API]
  B --> E{You Are Here}
  style E fill:#f90,color:#fff

Project Overview

Build a rate limiting system with:

  1. Global per-IP fixed window limiting (60 req/min)
  2. Per-user token bucket limiting (burst 100, sustained 10/s)
  3. Login brute force protection (5 attempts per 15 min)
  4. Redis-backed storage
  5. Rate limit headers on all responses
  6. Graduated penalties for repeat offenders

Step 1: Setup

npm init -y
npm install express redis ioredis rate-limit-redis express-rate-limit
const express = require("express");
const Redis = require("ioredis");
const rateLimit = require("express-rate-limit");
const RedisStore = require("rate-limit-redis");

const app = express();
const redis = new Redis(process.env.REDIS_URL || "redis://localhost:6379");

Step 2: Global Rate Limiter

const globalLimiter = rateLimit({
  store: new RedisStore({
    sendCommand: (...args) => redis.call(...args)
  }),
  windowMs: 60000,
  max: 60,
  standardHeaders: true,
  legacyHeaders: false,
  message: { error: "Global rate limit exceeded. Max 60 requests per minute." }
});

app.use(globalLimiter);

Step 3: User Token Bucket Limiter

class TokenBucketLimiter {
  constructor(redis) {
    this.redis = redis;
    this.script = `
      local key = KEYS[1]
      local now = tonumber(ARGV[1])
      local capacity = tonumber(ARGV[2])
      local refillRate = tonumber(ARGV[3])
      local refillInterval = tonumber(ARGV[4])

      local data = redis.call("HMGET", key, "tokens", "lastRefill")
      local tokens = tonumber(data[1]) or capacity
      local lastRefill = tonumber(data[2]) or now

      local elapsed = now - lastRefill
      local refill = math.floor(elapsed / refillInterval) * refillRate
      tokens = math.min(capacity, tokens + refill)

      if tokens >= 1 then
        tokens = tokens - 1
        redis.call("HMSET", key, "tokens", tokens, "lastRefill", now)
        redis.call("EXPIRE", key, 86400)
        return {1, tokens}
      end

      redis.call("HMSET", key, "tokens", tokens, "lastRefill", now)
      return {0, tokens}
    `;
  }

  async check(userId) {
    const key = `bucket:${userId}`;
    const result = await this.redis.eval(
      this.script, 1, key,
      Date.now(), 100, 10, 1000
    );
    return { allowed: result[0] === 1, remaining: result[1] };
  }

  middleware() {
    return async (req, res, next) => {
      if (!req.user) return next();
      const result = await this.check(req.user.id);
      if (!result.allowed) {
        return res.status(429).json({
          error: "User rate limit exceeded",
          remaining: result.remaining
        });
      }
      next();
    };
  }
}

const bucketLimiter = new TokenBucketLimiter(redis);

Step 4: Login Protection

const loginLimiter = rateLimit({
  store: new RedisStore({
    sendCommand: (...args) => redis.call(...args)
  }),
  windowMs: 900000,
  max: 5,
  skipSuccessfulRequests: true,
  keyGenerator: (req) => `login:${req.body?.username || req.ip}`,
  message: { error: "Too many login attempts. Try again in 15 minutes." }
});

app.post("/login", loginLimiter, (req, res) => {
  if (req.body.username === "admin" && req.body.password === "secret") {
    const token = jwt.sign({ id: 1, tier: "pro" }, "secret");
    return res.json({ token });
  }
  res.status(401).json({ error: "Invalid credentials" });
});

Step 5: Graduated Penalties

async function getPenaltyMultiplier(ip) {
  const violations = await redis.get(`penalty:${ip}`);
  if (!violations) return 1;
  const count = parseInt(violations);
  return Math.max(0.1, 1 / Math.pow(2, count - 1));
}

app.use(async (req, res, next) => {
  const multiplier = await getPenaltyMultiplier(req.ip);
  req.limitMultiplier = multiplier;
  next();
});

function penalizedLimiter(baseLimit) {
  return (req, res, next) => {
    const effectiveLimit = Math.max(1, Math.floor(baseLimit * req.limitMultiplier));
    // Apply limiting with effectiveLimit
    next();
  };
}

Step 6: Auth Middleware

function auth(req, res, next) {
  const token = req.headers.authorization?.split(" ")[1];
  if (token) {
    try {
      req.user = jwt.verify(token, "secret");
    } catch {}
  }
  next();
}

app.use(auth);

Step 7: Complete Pipeline

app.use(globalLimiter);
app.use(bucketLimiter.middleware());

app.get("/api/data", (req, res) => {
  res.json({ data: "success", user: req.user?.id });
});

app.use((err, req, res, next) => {
  console.error("Error:", err.message);
  res.status(500).json({ error: "Internal server error" });
});

app.listen(3000);

Step 8: Test the System

# Test global limit
for i in $(seq 1 65); do curl -s http://localhost:3000/api/data | head -c 50; echo; done

# Test login limit
for i in $(seq 1 6); do
  curl -X POST http://localhost:3000/login \
    -H "Content-Type: application/json" \
    -d '{"username":"admin","password":"wrong"}'
  echo
done

Expected output after 5 failed logins:

{"error": "Too many login attempts. Try again in 15 minutes."}

Common Mistakes

  1. Middleware order -- Auth middleware must run before user-specific rate limiters.

  2. Not testing with multiple users -- Test with different user IDs and IPs to verify isolation.

  3. Forgetting Redis connection error handling -- The system should degrade gracefully if Redis is unavailable.

  4. Not documenting rate limit policies -- Clients need to know limits. Document them in your API reference.

  5. Setting penalties too aggressively -- First offense should be a warning, not a full block.

Practice Questions

  1. Why is the global limiter applied before the user limiter? Global limits block bad actors early. User limits provide finer-grained control for authenticated users.

  2. How would you add organization-level rate limiting? Add another dimension with org:${orgId} as the key and a separate limit configuration.

  3. What changes for a Serverless deployment? Use a managed Redis service. Each function invocation creates a new connection or uses connection pooling.

  4. Challenge: Add Websocket rate limiting to the system.

FAQ

How do I migrate from in-memory to Redis rate limiting?

Replace the memory store with RedisStore. The API is identical. Migrate gradually by running both in parallel.

How do I monitor rate limiting effectiveness?

Track 429 response rates, blocked IP counts, and abuse detection alerts in your monitoring system.

Can I use this system with GraphQL?

Yes. Rate limit by API key or user ID. Consider query complexity-based limiting for GraphQL.

How do I handle rate limiting in microservices?

Use a shared Redis cluster. Each service checks limits independently against the shared store.

What is the best deployment strategy?

Start with global and user limits. Add endpoint-specific and security limits as needed. Monitor and adjust.

Project Extension Ideas

  1. Add a dashboard showing current rate limit usage
  2. Implement rate limit notifications for approaching limits
  3. Add organization-level limits for multi-tenant SaaS
  4. Implement query complexity-based Graphql rate limiting
  5. Add automatic IP reputation scoring

What's Next

Congratulations on completing the rate limiting project! Explore retry strategies for building resilient systems. Then learn about implementing exponential backoff.

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro