🌐 Detecting your location…
📢 Advertisement — Configure AdSense in Appearance → Customize → AdSense Settings

How to Implement Rate Limiting in a Node.js API: Complete 2026 Guide

⏱️4 min read  ·  811 words

Rate limiting protects your API from abuse, prevents accidental overload, and ensures fair usage across clients. Without it, a single client (or attacker) can overwhelm your server. This guide implements robust rate limiting in Node.js from simple to production-grade.

Why Rate Limit?

  • Prevent abuse: Stop brute-force attacks and scraping
  • Ensure fairness: No single client monopolizes resources
  • Control costs: Limit expensive operations (AI calls, DB queries)
  • Protect stability: Prevent accidental or malicious overload

The Main Algorithms

Algorithm How It Works Trade-off
Fixed Window N requests per fixed time window Simple, but allows bursts at window edges
Sliding Window N requests in the last X seconds (rolling) Smoother, slightly more complex
Token Bucket Tokens refill over time; each request costs one Allows controlled bursts, flexible

Simple: express-rate-limit

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

const limiter = rateLimit({
  windowMs: 15 * 60 * 1000,   // 15 minutes
  max: 100,                    // 100 requests per window per IP
  standardHeaders: true,       // return RateLimit-* headers
  legacyHeaders: false,
  message: { error: 'Too many requests, please try again later.' },
});

// Apply globally
app.use(limiter);

// Or stricter limits on specific routes
const authLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 5,   // only 5 login attempts per 15 min
});
app.post('/login', authLimiter, loginHandler);

Production: Redis-Backed Rate Limiting

In-memory limits don’t work across multiple server instances. Redis shares limits across all instances:

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

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

const limiter = rateLimit({
  store: new RedisStore({
    sendCommand: (...args) => redis.call(...args),
  }),
  windowMs: 15 * 60 * 1000,
  max: 100,
});

app.use(limiter);
// Now all server instances share the same rate-limit counters

Custom Token Bucket with Redis

async function tokenBucket(key, maxTokens, refillRate) {
  const now = Date.now();
  const bucket = await redis.hgetall(`bucket:${key}`);

  let tokens = parseFloat(bucket.tokens ?? maxTokens);
  let lastRefill = parseInt(bucket.lastRefill ?? now);

  // Refill tokens based on elapsed time
  const elapsed = (now - lastRefill) / 1000;
  tokens = Math.min(maxTokens, tokens + elapsed * refillRate);

  if (tokens < 1) {
    return { allowed: false, retryAfter: (1 - tokens) / refillRate };
  }

  tokens -= 1;  // consume one token
  await redis.hset(`bucket:${key}`, { tokens, lastRefill: now });
  await redis.expire(`bucket:${key}`, 3600);

  return { allowed: true, remaining: Math.floor(tokens) };
}

// Middleware
async function rateLimitMiddleware(req, res, next) {
  const result = await tokenBucket(req.ip, 10, 1);  // 10 tokens, 1/sec refill
  if (!result.allowed) {
    res.setHeader('Retry-After', Math.ceil(result.retryAfter));
    return res.status(429).json({ error: 'Rate limit exceeded' });
  }
  res.setHeader('X-RateLimit-Remaining', result.remaining);
  next();
}

Per-User (Not Just Per-IP) Limiting

// Rate limit by authenticated user ID instead of IP
const userLimiter = rateLimit({
  windowMs: 60 * 1000,
  max: 60,
  keyGenerator: (req) => {
    // Use user ID if authenticated, fall back to IP
    return req.user?.id || req.ip;
  },
  store: new RedisStore({ sendCommand: (...args) => redis.call(...args) }),
});

// Tiered limits based on plan
function getLimitForUser(req) {
  const plan = req.user?.plan || 'free';
  return { free: 100, pro: 1000, enterprise: 10000 }[plan];
}

Setting Proper Response Headers

// Standard rate-limit headers help clients back off gracefully
res.setHeader('RateLimit-Limit', limit);
res.setHeader('RateLimit-Remaining', remaining);
res.setHeader('RateLimit-Reset', resetTimestamp);

// On limit exceeded, always include Retry-After
res.status(429)
   .setHeader('Retry-After', secondsUntilReset)
   .json({ error: 'Too many requests' });

Best Practices

  • Use Redis for multi-instance apps — in-memory limits don't sync across servers
  • Stricter limits on sensitive endpoints — login, password reset, and payment endpoints need tight limits
  • Return clear headers — RateLimit-* and Retry-After help clients behave well
  • Rate limit by user when authenticated — fairer than IP-only (shared IPs, proxies)
  • Consider tiered limits — different limits for free vs paid users
  • Combine with other defenses — rate limiting is one layer, not a complete security solution

Frequently Asked Questions

Q: Which algorithm should I use?
A: Token bucket for flexibility (allows controlled bursts), sliding window for smoothness, fixed window for simplicity. For most APIs, express-rate-limit's default (fixed/sliding window) is fine. Token bucket suits APIs where occasional bursts are acceptable.

Q: Should I rate limit by IP or user?
A: By user ID when authenticated (fairer, handles shared IPs), falling back to IP for anonymous requests. IP-only limiting can unfairly affect users behind shared NATs or corporate proxies.

Q: Why does my rate limit reset unexpectedly?
A: With in-memory limiting across multiple instances, each server has its own counter — a client hitting different instances sees inconsistent limits. Use Redis to share state across all instances.

Q: What status code should I return?
A: 429 Too Many Requests, with a Retry-After header telling the client how long to wait. This is the standard and well-behaved clients respect it.

Q: Can rate limiting stop DDoS attacks?
A: It helps against application-layer abuse but isn't a complete DDoS defense. For large-scale attacks, use a CDN/WAF (Cloudflare, AWS Shield) at the network edge in addition to application rate limiting.

Conclusion

Rate limiting is essential for protecting your Node.js API from abuse and overload. Start with express-rate-limit for simple cases, and move to Redis-backed limiting for multi-instance production apps so limits sync across servers. Apply stricter limits to sensitive endpoints (login, payments), rate limit by user when authenticated, and always return clear RateLimit-* and Retry-After headers so clients back off gracefully. Combine it with a CDN/WAF for defense in depth.

✍️ Leave a Comment

Your email address will not be published. Required fields are marked *

🌐 Read in:🇬🇧 English🇩🇪 Deutsch🇧🇷 Português🇸🇦 العربية🇮🇳 हिन्दी🇧🇩 বাংলা