Why Your API Needs Rate Limiting (And How to Do It Right)



This content originally appeared on DEV Community and was authored by shiva shanker

Your API got hammered by bots? Server bills through the roof? Been there! Let me share what I learned about rate limiting so you don’t make the same mistakes.

TL;DR (Too Long; Didn’t Read)

Rate limiting = API bouncer. Controls requests per time window. Essential for performance, cost control, and preventing abuse. Easy to implement with express-rate-limit.

What’s Rate Limiting?

Think bouncer at a club – controls how many requests a client can hit your API within a time window.

Examples:

  • 100 requests/minute per user
  • 1000 requests/hour per IP
  • 10,000 requests/day for premium users

Why You Need It (Trust Me)

1. Prevent API Abuse
Bots can spam thousands of requests and crash your server. Seen it happen too many times.

2. Fair Usage
One heavy user shouldn’t slow down everyone else.

3. Cost Control
Each API call = money (server resources, DB queries, third-party APIs).

4. Better Performance
Less spam = faster API for real users.

Quick Implementation

Node.js + Express (easiest way):

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

const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutes
  max: 100, // limit each IP to 100 requests per windowMs
  message: 'Too many requests, please try again later.'
});

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

Boom! Your API is now protected.

Advanced Patterns

Different limits for different endpoints:

// Strict for auth
const authLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 5 // only 5 login attempts per 15 min
});

// Relaxed for public data
const publicLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 1000
});

app.use('/api/auth/', authLimiter);
app.use('/api/public/', publicLimiter);

Redis for distributed systems:

const RedisStore = require('rate-limit-redis');
const redis = require('redis');
const client = redis.createClient();

const limiter = rateLimit({
  store: new RedisStore({
    client: client,
  }),
  windowMs: 15 * 60 * 1000,
  max: 100
});

Pro Tips From Experience

✅ DO:

  • Start with conservative limits, adjust based on real usage
  • Use different limits for different endpoints
  • Provide clear error messages with retry info
  • Monitor and log rate limit hits

❌ DON’T:

  • Set limits too low (frustrated users)
  • Set limits too high (no protection)
  • Forget to whitelist monitoring tools
  • Ignore mobile app retry behavior

Better Error Responses

Instead of generic “Too many requests”:

const limiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 100,
  message: {
    error: 'Rate limit exceeded',
    retryAfter: '15 minutes',
    limit: 100,
    windowMs: 15 * 60 * 1000
  }
});

Testing Rate Limits

Quick script to test your limits:

// test-rate-limit.js
const axios = require('axios');

async function testRateLimit() {
  for (let i = 0; i < 105; i++) {
    try {
      const response = await axios.get('http://localhost:3000/api/test');
      console.log(`Request ${i + 1}: ${response.status}`);
    } catch (error) {
      console.log(`Request ${i + 1}: ${error.response.status} - ${error.response.data.message}`);
    }
  }
}

testRateLimit();

Real-World Example

Here’s how I implemented it for a small startup:

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

// Different tiers
const freeTierLimit = rateLimit({
  store: new MongoStore({
    uri: process.env.MONGODB_URI,
    collectionName: 'rateLimits',
  }),
  windowMs: 24 * 60 * 60 * 1000, // 24 hours
  max: 1000, // 1000 requests per day for free users
  keyGenerator: (req) => req.user.id
});

const premiumTierLimit = rateLimit({
  store: new MongoStore({
    uri: process.env.MONGODB_URI,
    collectionName: 'rateLimits',
  }),
  windowMs: 24 * 60 * 60 * 1000,
  max: 10000, // 10k requests per day for premium
  keyGenerator: (req) => req.user.id
});

// Apply based on user tier
app.use('/api/', (req, res, next) => {
  if (req.user.tier === 'premium') {
    premiumTierLimit(req, res, next);
  } else {
    freeTierLimit(req, res, next);
  }
});

Monitoring Rate Limits

Track important metrics:

const limiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 100,
  onLimitReached: (req) => {
    console.log(`Rate limit hit for IP: ${req.ip}`);
    // Send to monitoring service
    analytics.track('rate_limit_exceeded', {
      ip: req.ip,
      endpoint: req.path,
      userAgent: req.get('User-Agent')
    });
  }
});

Common Gotchas

  1. Mobile apps retry automatically – factor this into your limits
  2. Load balancers can mess with IP detection – use proper headers
  3. Don’t rate limit health checks – whitelist monitoring endpoints
  4. Consider user experience – show remaining requests in headers

Useful links:

Conclusion

Rate limiting isn’t optional anymore. Even small APIs need protection. Start simple, monitor usage, adjust as needed.

Your server bills will thank you!

What’s your rate limiting setup? Share your configs in the comments!


This content originally appeared on DEV Community and was authored by shiva shanker