Next.js API Rate Limiting
Introduction
Rate limiting is an essential technique for protecting your web applications from various issues such as abuse, denial-of-service attacks, and excessive resource consumption. When building APIs with Next.js, implementing rate limiting helps ensure that your services remain available, responsive, and fair for all users.
In this tutorial, you'll learn:
- What API rate limiting is and why it's important
- Different approaches to implement rate limiting in Next.js
- How to create basic and advanced rate limiting solutions
- Best practices for rate limiting in production applications
What is API Rate Limiting?
Rate limiting restricts how many requests a user or IP address can make to your API within a specified time window. For example, you might want to limit clients to 100 requests per minute. When a client exceeds this limit, the server responds with a status code (typically HTTP 429 - Too Many Requests) and may include information about when the client can try again.
Why implement rate limiting?
- Prevent abuse: Stops malicious users from overwhelming your API
- Resource management: Ensures fair distribution of server resources
- Cost control: Helps manage usage-based infrastructure costs
- SLA maintenance: Ensures service level agreements can be maintained
- Security: Mitigates certain types of attacks like brute force attempts
Basic Rate Limiting in Next.js API Routes
Let's start with a simple in-memory rate limiter for a Next.js API route. This approach is perfect for getting started and understanding the concepts.
First, we'll create a simple counter that tracks requests by IP address:
// pages/api/limited-endpoint.js
const rateLimit = {
windowMs: 60 * 1000, // 1 minute in milliseconds
maxRequests: 5 // limit each IP to 5 requests per windowMs
};
// Store for tracking requests
const requestCounts = new Map();
export default function handler(req, res) {
// Get client's IP address
const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress;
// Initialize or retrieve request count for this IP
const currentTime = Date.now();
let requestInfo = requestCounts.get(ip) || { count: 0, resetTime: currentTime + rateLimit.windowMs };
// Reset count if time window has passed
if (currentTime > requestInfo.resetTime) {
requestInfo = {
count: 0,
resetTime: currentTime + rateLimit.windowMs
};
}
// Check if rate limit is exceeded
if (requestInfo.count >= rateLimit.maxRequests) {
// Return 429 Too Many Requests
return res.status(429).json({
error: 'Rate limit exceeded',
message: 'Too many requests, please try again later.',
retryAfter: Math.ceil((requestInfo.resetTime - currentTime) / 1000) // seconds until reset
});
}
// Increment request count and update the map
requestInfo.count += 1;
requestCounts.set(ip, requestInfo);
// Add rate limit headers to response
res.setHeader('X-RateLimit-Limit', rateLimit.maxRequests);
res.setHeader('X-RateLimit-Remaining', rateLimit.maxRequests - requestInfo.count);
res.setHeader('X-RateLimit-Reset', Math.ceil(requestInfo.resetTime / 1000)); // Unix timestamp
// Process the actual request
res.status(200).json({ message: 'API request successful', data: 'Your data here' });
}
This basic implementation:
- Creates a Map to store request counts by IP address
- Defines a time window and maximum requests allowed
- Checks if a client has exceeded the limit
- Returns appropriate headers and status codes
- Resets counters after the time window expires
Testing the Basic Rate Limiter
If you make 6 requests in a row to this endpoint within a minute, you'll see:
For the first 5 requests:
{
"message": "API request successful",
"data": "Your data here"
}
And for the 6th request:
{
"error": "Rate limit exceeded",
"message": "Too many requests, please try again later.",
"retryAfter": 45 // Seconds until reset (will vary)
}
Creating a Reusable Rate Limiting Middleware
The above example is simple but doesn't follow DRY (Don't Repeat Yourself) principles. Let's create a reusable middleware for rate limiting:
// lib/rate-limit.js
export default function rateLimit({ limit = 10, windowMs = 60 * 1000 } = {}) {
const requestCounts = new Map();
return function rateLimitMiddleware(req, res, next) {
const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress;
const currentTime = Date.now();
let requestInfo = requestCounts.get(ip) || { count: 0, resetTime: currentTime + windowMs };
if (currentTime > requestInfo.resetTime) {
requestInfo = {
count: 0,
resetTime: currentTime + windowMs
};
}
if (requestInfo.count >= limit) {
res.setHeader('Retry-After', Math.ceil((requestInfo.resetTime - currentTime) / 1000));
return res.status(429).json({
error: 'Rate limit exceeded',
message: 'Too many requests, please try again later.'
});
}
requestInfo.count += 1;
requestCounts.set(ip, requestInfo);
res.setHeader('X-RateLimit-Limit', limit);
res.setHeader('X-RateLimit-Remaining', limit - requestInfo.count);
res.setHeader('X-RateLimit-Reset', Math.ceil(requestInfo.resetTime / 1000));
if (next) {
next(); // For middleware usage in non-API routes
}
};
}
Now, we can use this middleware in any API route:
// pages/api/protected-endpoint.js
import rateLimit from '../../lib/rate-limit';
// Create limiter instance - allow 5 requests per minute
const limiter = rateLimit({ limit: 5, windowMs: 60 * 1000 });
export default function handler(req, res) {
// Apply rate limiting
limiter(req, res);
// If limiter allows request to continue, process it
// Note: limiter will have already sent 429 if limit was exceeded
res.status(200).json({ message: 'Protected API endpoint accessed successfully' });
}
Advanced Rate Limiting with Redis
For production applications, an in-memory solution isn't ideal because:
- Memory gets cleared when the server restarts
- It doesn't work across multiple server instances
- It can consume significant memory with many users
Let's implement a more robust solution using Redis:
First, install the required packages:
npm install ioredis ms
Next, create a Redis-based rate limiter:
// lib/redis-rate-limit.js
import Redis from 'ioredis';
import ms from 'ms';
// Initialize Redis client
const redis = new Redis(process.env.REDIS_URL || 'redis://localhost:6379');
export default function redisRateLimit({
limit = 10,
windowMs = 60 * 1000, // 1 minute
keyPrefix = 'ratelimit:'
} = {}) {
return async function rateLimitMiddleware(req, res) {
const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress;
const key = `${keyPrefix}${ip}`;
// Use Redis to get current count
const requests = await redis.incr(key);
// If this is the first request, set expiry
if (requests === 1) {
await redis.expire(key, windowMs / 1000);
}
// Get time to expiration
const ttl = await redis.ttl(key);
// Set rate limit headers
res.setHeader('X-RateLimit-Limit', limit);
res.setHeader('X-RateLimit-Remaining', Math.max(0, limit - requests));
res.setHeader('X-RateLimit-Reset', Math.floor(Date.now() / 1000) + ttl);
// If rate limit exceeded, return 429 status
if (requests > limit) {
res.setHeader('Retry-After', ttl);
return res.status(429).json({
error: 'Rate limit exceeded',
message: 'Too many requests, please try again later.',
retryAfter: ttl
});
// Return true so we know the request was blocked
return true;
}
// Return false so we know the request can continue
return false;
};
}
Using the Redis-based limiter in an API route:
// pages/api/user/profile.js
import redisRateLimit from '../../../lib/redis-rate-limit';
// Create a rate limiter that allows 20 requests per minute
const limiter = redisRateLimit({
limit: 20,
windowMs: 60 * 1000,
keyPrefix: 'ratelimit:profile:'
});
export default async function handler(req, res) {
// Apply rate limiting - if it returns true, request was blocked
const isLimited = await limiter(req, res);
if (isLimited) {
// Rate limit response already sent
return;
}
// Process the API request
res.status(200).json({
user: {
name: 'John Doe',
email: '[email protected]',
// other user data
}
});
}
Advanced Rate Limiting Strategies
Different Limits for Different Endpoints
You can customize rate limits based on the sensitivity or resource requirements of each endpoint:
// pages/api/auth/login.js
import redisRateLimit from '../../../lib/redis-rate-limit';
// Stricter rate limit for login attempts to prevent brute force attacks
const limiter = redisRateLimit({
limit: 5,
windowMs: 5 * 60 * 1000, // 5 minutes
keyPrefix: 'ratelimit:login:'
});
export default async function handler(req, res) {
if (req.method !== 'POST') {
return res.status(405).json({ message: 'Method not allowed' });
}
const isLimited = await limiter(req, res);
if (isLimited) return;
// Process login logic
// ...
}
User-Based Rate Limiting
For authenticated routes, you might want to rate limit based on user ID rather than IP:
// lib/user-rate-limit.js
import Redis from 'ioredis';
const redis = new Redis(process.env.REDIS_URL);
export default function userRateLimit({
limit = 100,
windowMs = 60 * 60 * 1000, // 1 hour
keyPrefix = 'user-ratelimit:'
} = {}) {
return async function userRateLimitMiddleware(req, res) {
// Get user ID from authentication
const userId = req.user?.id;
// Fall back to IP-based limiting if no user ID
const identifier = userId || req.headers['x-forwarded-for'] || req.socket.remoteAddress;
const key = `${keyPrefix}${identifier}`;
// Rest of implementation similar to previous Redis example
// ...
};
}
Tiered Rate Limiting
For applications with different user tiers or subscription levels:
// lib/tiered-rate-limit.js
export default function tieredRateLimit() {
return async function tieredRateLimitMiddleware(req, res) {
// Get user from request
const user = req.user;
let limit, windowMs;
// Set limits based on user tier
switch (user?.tier) {
case 'premium':
limit = 1000;
windowMs = 60 * 1000; // 1 minute
break;
case 'basic':
limit = 100;
windowMs = 60 * 1000; // 1 minute
break;
default:
// Free tier or unauthenticated
limit = 20;
windowMs = 60 * 1000; // 1 minute
}
// Apply rate limiting with determined limits
// ...
};
}
Using Third-Party Libraries
For production applications, consider using battle-tested libraries:
Example with Express-Rate-Limit
First, install the packages:
npm install express-rate-limit
Then create a Next.js API middleware:
// pages/api/limited-with-library.js
import rateLimit from 'express-rate-limit';
import { NextApiRequest, NextApiResponse } from 'next';
// Create limiter
const limiter = rateLimit({
windowMs: 60 * 1000, // 1 minute
max: 10, // 10 requests per window
message: { error: 'Too many requests, please try again later.' },
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
legacyHeaders: false, // Disable the `X-RateLimit-*` headers
});
// Apply to Next.js API route
export default function handler(req, res) {
return new Promise((resolve, reject) => {
// Pass req/res to the limiter function
limiter(req, res, (result) => {
if (result instanceof Error) {
return reject(result);
}
// Your API logic here
res.status(200).json({ message: 'Request successful!' });
return resolve();
});
});
}
Best Practices for Rate Limiting
-
Be transparent: Clearly communicate your rate limits in documentation and via HTTP headers.
-
Use appropriate time windows: Short windows (seconds/minutes) for preventing abuse, longer windows (hours/days) for resource allocation.
-
Include helpful response headers:
X-RateLimit-Limit
: Maximum requests allowed in the windowX-RateLimit-Remaining
: Remaining requests in the current windowX-RateLimit-Reset
: Time when the current window resets (Unix timestamp)Retry-After
: Seconds until client can retry
-
Implement graceful degradation: If a user exceeds their limit, consider serving cached or simplified responses instead of blocking entirely.
-
Monitor and adjust limits: Regularly review your rate limit settings based on actual usage patterns and server capacity.
-
Consider different limit types:
- Request count limits (e.g., 100 requests per minute)
- Concurrency limits (e.g., 5 requests at a time)
- Bandwidth limits (e.g., 1MB of data per minute)
Real-World Example: Multi-Tier API with Rate Limiting
Here's a complete example of an API that implements different rate limits for different endpoints and user types:
// pages/api/[...path].js
import redisRateLimit from '../../lib/redis-rate-limit';
import { getSession } from 'next-auth/react';
// Different limiters for different endpoints
const publicLimiter = redisRateLimit({ limit: 30, windowMs: 60 * 1000 });
const authLimiter = redisRateLimit({ limit: 100, windowMs: 60 * 1000 });
const sensitiveActionLimiter = redisRateLimit({ limit: 5, windowMs: 60 * 1000 });
export default async function handler(req, res) {
// Get path from URL
const path = req.query.path || [];
const endpoint = path.join('/');
// Get user session
const session = await getSession({ req });
const isAuthenticated = !!session;
// Apply different rate limits based on endpoint and auth status
let isLimited = false;
if (endpoint.startsWith('auth/') || endpoint === 'login') {
// Stricter limits for authentication endpoints
isLimited = await sensitiveActionLimiter(req, res);
} else if (isAuthenticated) {
// Higher limits for authenticated users
isLimited = await authLimiter(req, res);
} else {
// Stricter limits for unauthenticated users
isLimited = await publicLimiter(req, res);
}
if (isLimited) {
// Rate limiter has already sent response
return;
}
// Handle API request based on path
switch (endpoint) {
case 'public/stats':
return res.status(200).json({ visits: 12345, popularPages: ['/', '/about'] });
case 'user/profile':
if (!isAuthenticated) {
return res.status(401).json({ error: 'Authentication required' });
}
return res.status(200).json({ user: session.user });
// Other endpoints...
default:
return res.status(404).json({ error: 'Endpoint not found' });
}
}
Summary
API rate limiting is an essential technique for building robust, secure, and fair web applications. In this tutorial, you've learned:
- What rate limiting is and why it's important
- How to implement basic in-memory rate limiting
- Creating reusable rate limiting middleware
- Advanced techniques using Redis for distributed applications
- Different rate limiting strategies for various use cases
- Best practices for production environments
By implementing appropriate rate limiting, you can protect your Next.js APIs from abuse, ensure fair resource distribution, and maintain reliable service for all users.
Additional Resources
- MDN HTTP 429 Status Code
- IETF RateLimit Header Fields for HTTP
- Redis Documentation
- Next.js API Routes Documentation
Exercise Ideas
- Implement a basic rate limiter that counts requests per IP address.
- Modify the Redis rate limiter to use different time windows based on the endpoint.
- Create a system that increases rate limits for authenticated users.
- Build a rate limiter that tracks usage across days and resets at midnight.
- Implement a system that sends warning emails to users approaching their API usage limits.
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)