How to Build a Distributed Rate Limiting System Using Redis and Lua Scripts
How to Build a Distributed Rate Limiting System Using Redis and Lua Scripts êŽë š
In this comprehensive guide, youâll build a distributed rate limiter using Redis and Lua scripting to control user requests in a high-traffic environment.
Rate limiting is crucial in any system to prevent abuse, manage traffic, and protect your resources. By leveraging Redis and Lua, you'll build an efficient, scalable rate limiting system that can handle a large number of requests while keeping your backend services safe.
We will also include an interactive demo where users can simulate traffic, observe rate limits being enforced, and view logs of blocked requests.
What You Will Learn
- How to build a rate limiting system using Redis.
- How to use Lua scripts with Redis to achieve atomic operations.
- Understanding Redis data structures for efficient request tracking.
- Techniques for handling high traffic in a distributed system.
- Using Docker to simulate and scale a distributed rate limiter.
Prerequisites
Before starting, ensure you have the following installed:
- Node.js (v14 or higher)
- Redis
- Docker (for simulating a distributed environment)
- Basic understanding of Node.js, Redis, and Lua scripting.
Project Overview
In this tutorial, you will:
- Build a rate limiter using Redis and Lua to enforce request quotas.
- Use Lua scripts to ensure atomic operations, avoiding race conditions.
- Implement a token bucket algorithm for rate limiting.
- Create an interactive demo to simulate high traffic and visualize rate limiting in action.
System Architecture
You'll build the system with the following components:
- API Server: Handles incoming user requests.
- Redis: Stores request data and enforces rate limits.
- Lua Scripts: Ensures atomic updates to Redis for rate limiting.
- Docker: Simulates a distributed environment with multiple instances.
Step 1: How to Set Up the Project
Let's start by setting up our Node.js project:
mkdir distributed-rate-limiter
cd distributed-rate-limiter
npm init -y
Next, install the required dependencies:
npm install express redis dotenv
- express: A lightweight web server framework.
- redis: For interacting with Redis.
- dotenv: For managing environment variables.
Create a .env
file with the following content:
REDIS_HOST=localhost
REDIS_PORT=6379
PORT=3000
RATE_LIMIT=5
TIME_WINDOW=60
These variables define the Redis host, port, rate limit (number of allowed requests), and the time window (in seconds).
Step 2: How to Set Up Redis
Before we dive into the code, ensure that Redis is installed and running on your system. If you donât have Redis installed, you can use Docker to quickly set it up:
docker run -p 6379:6379 \
--name redis-rate-limiter \
-d redis
Step 3: How to Implement the Rate Limiter with Redis and Lua
To efficiently handle rate limiting, we'll use a token bucket algorithm. In this algorithm:
- Each user has a âbucketâ of tokens.
- Each request consumes a token.
- Tokens refill periodically at a set rate.
To ensure atomicity and avoid race conditions, we'll use Lua scripting with Redis. Lua scripts in Redis execute atomically, which means they canât be interrupted by other operations while running.
How to Create a Lua Script for Rate Limiting
Create a file called rate_limiter.lua
:
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local current = redis.call("get", key)
if current and tonumber(current) >= limit then
return 0
else
if current then
redis.call("incr", key)
else
redis.call("set", key, 1, "EX", window)
end
return 1
end
- Inputs:
KEYS[1]
: The Redis key representing the userâs request count.ARGV[1]
: The rate limit (maximum number of allowed requests).ARGV[2]
: The time window (in seconds) for the rate limit.
- Logic:
- If the user has reached the rate limit, return
0
(request blocked). - If the user is within the limit, increment their request count or set a new count with an expiration if it's the first request.
- Return 1 (request allowed).
- If the user has reached the rate limit, return
Step 4: How to Create the Node.js API Server
Create a file called server.js
:
require('dotenv').config();
const express = require('express');
const redis = require('redis');
const fs = require('fs');
const path = require('path');
const app = express();
const client = redis.createClient({
host: process.env.REDIS_HOST,
port: process.env.REDIS_PORT
});
const rateLimitScript = fs.readFileSync(path.join(__dirname, 'rate_limiter.lua'), 'utf8');
const RATE_LIMIT = parseInt(process.env.RATE_LIMIT);
const TIME_WINDOW = parseInt(process.env.TIME_WINDOW);
// Middleware for rate limiting
async function rateLimiter(req, res, next) {
const ip = req.ip;
try {
const allowed = await client.eval(rateLimitScript, 1, ip, RATE_LIMIT, TIME_WINDOW);
if (allowed === 1) {
next();
} else {
res.status(429).json({ message: 'Too many requests. Please try again later.' });
}
} catch (err) {
console.error('Error in rate limiter:', err);
res.status(500).json({ message: 'Internal server error' });
}
}
app.use(rateLimiter);
app.get('/', (req, res) => {
res.send('Welcome to the Rate Limited API!');
});
const PORT = process.env.PORT;
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
- Rate Limiter Middleware:
- Retrieves the client's IP address and checks if they are within the rate limit using the Lua script.
- If the user exceeds the limit, a
429
response is sent.
- API Endpoint:
- The root endpoint is rate-limited, so users can only access it a limited number of times within the specified window.
Step 5: How to Test the Rate Limiter
Start Redis
docker start redis-rate-limiter
Run the Node.js Server
node server.js
Simulate Requests
- Use
curl
or Postman to test the rate limiter:
curl http://localhost:3000
- Send multiple requests rapidly to see rate limiting in action.
Step 6: How to Visualize Rate Limiting Metrics
To monitor rate limiting metrics like cache hits and blocked requests, we'll add logging to the middleware in server.js
:
async function rateLimiter(req, res, next) {
const ip = req.ip;
try {
const allowed = await client.eval(rateLimitScript, 1, ip, RATE_LIMIT, TIME_WINDOW);
if (allowed === 1) {
console.log(`Allowed request from ${ip}`);
next();
} else {
console.log(`Blocked request from ${ip}`);
res.status(429).json({ message: 'Too many requests. Please try again later.' });
}
} catch (err) {
console.error('Error in rate limiter:', err);
res.status(500).json({ message: 'Internal server error' });
}
}
Step 7: How to Deploy with Docker
Letâs containerize the application to run it in a distributed environment.
Create a Dockerfile
:
FROM node:14
WORKDIR /app
COPY . .
RUN npm install
EXPOSE 3000
CMD ["node", "server.js"]
Build and Run the Docker Container:
docker build -t rate-limiter .
docker run -p 3000:3000 rate-limiter
Now you can scale the rate limiter by running multiple instances.
Conclusion: What Youâve Learned
Congratulations! Youâve successfully built a distributed rate limiter using Redis and Lua scripts. Throughout this tutorial, youâve learned how to:
- Implement rate limiting to control user requests in a distributed system.
- Use Lua scripts in Redis to perform atomic operations.
- Apply a token bucket algorithm to manage request quotas.
- Monitor rate limiting metrics to optimize performance.
- Use Docker to simulate a scalable distributed environment.
Next Steps
- Add Rate Limiting by User ID: Extend the system to support rate limits per user.
- Integrate with Nginx: Use Nginx as a reverse proxy with Redis-backed rate limiting.
- Deploy with Kubernetes: Scale your rate limiter using Kubernetes for high availability.
Happy coding!