I’ve been thinking a lot lately about how to protect web services from traffic spikes and abuse. It’s not just about stopping bad actors—it’s also about ensuring that all users get fair access, especially during peak times. That’s where rate limiting comes in. In this guide, I’ll walk you through building a production-ready rate limiting system using Redis, Node.js, and TypeScript. Stick with me—by the end, you’ll have a solid, reusable solution you can deploy with confidence.
Rate limiting is more than just counting requests. It’s a way to control how often a client can interact with your API within a specific timeframe. Without it, your service could become overwhelmed, slow, or even crash under heavy load. But how do you enforce these limits consistently, especially when your application runs across multiple servers?
Redis is my go-to tool for this. It’s fast, supports atomic operations, and handles expiration natively. Plus, its in-memory nature makes it perfect for high-throughput scenarios like rate limiting. Have you ever wondered how platforms like Twitter or GitHub manage to serve millions of users without buckling under pressure? A lot of it comes down to smart rate limiting.
Let’s start by setting up our project. We’ll use Express for the web server, ioredis for Redis interactions, and TypeScript for type safety. Here’s how to initialize the project:
mkdir redis-rate-limiter
cd redis-rate-limiter
npm init -y
npm install express redis ioredis
npm install -D typescript @types/node @types/express
Next, we define our core types to keep things organized and type-safe:
export interface RateLimitResult {
allowed: boolean;
remaining: number;
resetTime: number;
}
Now, let’s talk algorithms. There are a few popular approaches, each with its strengths. The Token Bucket algorithm is great for handling bursts—imagine a bucket that refills slowly but can handle a sudden surge up to its capacity. The Sliding Window algorithm offers more precision by tracking requests continuously. And the Fixed Window approach is simple but effective for many use cases.
Which one should you choose? It depends on your needs. If you expect traffic spikes, Token Bucket might be your best bet. If you need strict, smooth limits, consider Sliding Window.
Here’s a basic implementation of the Token Bucket algorithm using Redis Lua scripting for atomicity:
const luaScript = `
local key = KEYS[1]
local bucketSize = tonumber(ARGV[1])
local refillRate = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local tokensRequested = tonumber(ARGV[4])
local data = redis.call('HMGET', key, 'tokens', 'lastRefill')
local currentTokens = tonumber(data[1]) or bucketSize
local lastRefill = tonumber(data[2]) or now
local timePassed = math.max(0, now - lastRefill)
local tokensToAdd = (timePassed / 1000) * refillRate
currentTokens = math.min(bucketSize, currentTokens + tokensToAdd)
local allowed = currentTokens >= tokensRequested
if allowed then
currentTokens = currentTokens - tokensRequested
end
redis.call('HMSET', key, 'tokens', currentTokens, 'lastRefill', now)
redis.call('EXPIRE', key, math.ceil(bucketSize / refillRate) * 2)
return { allowed and 1 or 0, currentTokens }
`;
This script ensures that all operations—checking, updating, and expiring—happen atomically in Redis. No race conditions, no surprises.
But what if you need to support multiple strategies or change limits on the fly? That’s where middleware comes in. Let’s build a flexible rate limiter that can be easily integrated into an Express app:
import express from 'express';
import { Redis } from 'ioredis';
const app = express();
const redis = new Redis();
app.use(async (req, res, next) => {
const identifier = req.ip; // or use API keys, user IDs, etc.
const result = await checkRateLimit(redis, identifier, 100, 60000); // 100 requests per minute
if (!result.allowed) {
return res.status(429).json({ error: 'Too many requests' });
}
res.set('X-RateLimit-Remaining', result.remaining.toString());
next();
});
This middleware checks the rate limit for each request based on the client’s IP address. It’s simple but effective. You can extend it to support user-specific limits, different endpoints, or even burst handling.
Speaking of advanced features, have you considered how to monitor your rate limiter in production? Logging, metrics, and alerting are crucial. You might want to track how often limits are hit, which clients are most active, and whether your configuration needs tuning.
Deploying this system requires attention to error handling and resilience. What happens if Redis goes down? You might decide to fail open (allow all requests) or fail closed (block everything). Neither is perfect, but planning for failure is key.
Here’s a tip: use Redis clusters for high availability. And always set appropriate timeouts and retries for your Redis connections.
I hope this guide gives you a clear path to implementing robust rate limiting. It’s one of those things you don’t appreciate until you need it—but when you do, you’ll be glad it’s there.
If you found this useful, feel free to share it with others who might benefit. Have questions or suggestions? Leave a comment below—I’d love to hear how you’re tackling rate limiting in your projects.