A Redis-backed rate limiter for Node.js and Express with support for Fixed Window, Sliding Window, and Token Bucket algorithms.
- Redis-backed rate limiting
- Three rate limiting algorithms:
- Fixed Window
- Sliding Window
- Token Bucket
- Atomic Redis operations using Lua scripts
- IP-based rate limiting
- Configurable rate limits and windows
- Standard rate-limit response headers
429 Too Many Requestsresponses- Centralized error handling
- Graceful Redis shutdown
- Docker support
- Concurrent request handling
- Automated tests with Vitest and Supertest
- TypeScript
- Node.js
- TypeScript
- Express
- Redis
- Docker
- Vitest
- Supertest
- dotenv
Limitra sits between the incoming HTTP request and the application route.
For each request, the middleware:
- Gets the client's IP address.
- Passes the IP to the configured rate limiter.
- The selected algorithm checks Redis to determine whether the request is allowed.
- Adds rate-limit headers to the response.
- If the limit is exceeded, returns a
429 Too Many Requestsresponse. - If the request is allowed, continues to the application route.
Divides time into fixed intervals.
For example, with a limit of 10 requests per 60 seconds:
- Requests 1–10 are allowed.
- Request 11 is rejected.
- The counter resets when the next window begins.
Redis stores a counter for each IP and time window.
Tracks individual request timestamps using a Redis Sorted Set.
Old requests are removed as they fall outside the configured window. This provides a more precise limit than Fixed Window and avoids the boundary burst that can occur with fixed windows.
Each client has a bucket containing tokens.
- The bucket has a maximum capacity.
- Tokens are added over time according to the configured refill rate.
- Each request consumes tokens.
- A request is rejected when there are not enough tokens.
This allows controlled bursts while maintaining a long-term request rate.
This project is currently provided as source code rather than an npm package.
To use the rate limiter in another Node.js/Express project:
- Copy the
src/rate-limiterdirectory into your project. - Copy the Redis client implementation.
- Install the required dependencies.
- Configure your Redis connection.
- Create a limiter using the desired algorithm.
- Attach the rate-limit middleware to the routes you want to protect.
Example:
import { getRateLimitConfig } from "./rate-limiter/config.js"
import { createRateLimiter } from "./rate-limiter/factory.js"
import { createRateLimitMiddleware } from "./rate-limiter/middleware.js"
const config = getRateLimitConfig()
const limiter = createRateLimiter(config)
app.use("/api", createRateLimitMiddleware(limiter))Make sure the following are installed on your system:
- Node.js
- npm
- Redis
You can verify the installations with:
node --version
npm --version
redis-server --versionClone the repository and move into the project directory:
git clone https://github.com/devsterxyz/limitra.git
cd limitraInstall the project dependencies:
npm installCreate a .env file in the project root:
RATE_LIMIT_ALGORITHM=fixed
RATE_LIMIT=10
WINDOW_SIZE=60
REFILL_RATE=1
REQUEST_COST=1
REDIS_URL=redis://localhost:6379| Variable | Description | Default |
|---|---|---|
RATE_LIMIT_ALGORITHM |
Rate limiting algorithm: fixed, sliding, or token-bucket |
fixed |
RATE_LIMIT |
Maximum requests allowed in a window / bucket capacity | 10 |
WINDOW_SIZE |
Fixed/Sliding Window size in seconds | 60 |
REFILL_RATE |
Number of tokens added per second for Token Bucket | 1 |
REQUEST_COST |
Number of tokens consumed by each request | 1 |
REDIS_URL |
Redis connection URL | redis://localhost:6379 |
Make sure Redis is running before starting the application.
For a local Redis installation:
redis-serverYou can verify that Redis is running with:
redis-cli pingYou should receive:
PONG
Start the application in development mode:
npm run devThe server will start at:
http://localhost:3000
Build the TypeScript project:
npm run buildThen start the compiled application:
npm startThe server will be available at:
http://localhost:3000
The project exposes three algorithm-specific test endpoints, one for each rate-limiting algorithm:
| Endpoint | Algorithm |
|---|---|
GET /api/test/fixed |
Fixed Window |
GET /api/test/sliding |
Sliding Window |
GET /api/test/token-bucket |
Token Bucket |
Send a request using curl:
curl http://localhost:3000/api/test/fixed
curl http://localhost:3000/api/test/sliding
curl http://localhost:3000/api/test/token-bucketA successful request returns:
{
"message": "Request allowed",
"algorithm": "fixed"
}Once the configured rate limit is exceeded, the server responds with:
HTTP/1.1 429 Too Many Requestswith the following response body:
{
"message": "Too many requests"
}To quickly verify that the rate limit is working, send requests to a specific algorithm endpoint:
for i in {1..15}; do
echo -n "Request $i: "
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:3000/api/test/fixed
done
### Run Tests
Run the complete test suite:
```bash
npm testTo run Vitest in watch mode:
npm run test:watchYou can verify that the TypeScript project builds successfully with:
npm run buildThe compiled output is generated in the dist directory.
Note: Docker setup is documented separately in the Docker section. The project supports running both the API and Redis through Docker Compose.
For every request processed by the rate limiter, the middleware can return the following headers:
| Header | Description |
|---|---|
X-RateLimit-Limit |
Maximum number of requests/tokens allowed |
X-RateLimit-Remaining |
Number of requests/tokens remaining |
X-RateLimit-Reset |
Number of seconds until the current limit can reset or a request can be retried |
Retry-After |
Number of seconds the client should wait before retrying after being rate limited |
When a request is allowed, the response includes the rate-limit information:
HTTP/1.1 200 OK
X-RateLimit-Limit: 10
X-RateLimit-Remaining: 9
X-RateLimit-Reset: 42The application route can then return its normal response:
{
"message": "Request allowed"
}HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 10
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 20
Retry-After: 20Response body:
{
"message": "Too many requests"
}The Retry-After header tells the client how many seconds it should wait before trying again.
If the rate limiter cannot communicate with Redis or another internal error occurs, the request is passed to the centralized error handler.
The server responds with:
HTTP/1.1 503 Service UnavailableResponse body:
{
"message": "Internal server error"
}Limitra includes Docker support for running both the API and Redis together using Docker Compose.
Make sure Docker and Docker Compose are installed:
docker --version
docker compose versionBuild and start the containers:
docker compose up --buildThis starts:
- Limitra API
- Redis
The API will be available at:
http://localhost:3000
Redis will be available on:
localhost:6379
To start the containers in detached mode:
docker compose up --build -ddocker compose psBoth the API and Redis containers should be running.
To view the API logs:
docker compose logs apiTo view Redis logs:
docker compose logs redisTo follow the logs in real time:
docker compose logs -fOnce the containers are running, test each algorithm endpoint:
curl http://localhost:3000/api/test/fixed
curl http://localhost:3000/api/test/sliding
curl http://localhost:3000/api/test/token-bucketA successful request returns:
{
"message": "Request allowed",
"algorithm": "fixed"
}After the configured rate limit is exceeded, the API returns:
HTTP/1.1 429 Too Many RequestsTo stop the running containers:
docker compose downThis stops and removes the containers while keeping the Docker images available for future runs.
If you make changes to the application code or Docker configuration, rebuild the containers with:
docker compose up --buildlimitra/
├── src/
│ ├── rate-limiter/
│ │ ├── fixed-window.ts
│ │ ├── sliding-window.ts
│ │ ├── token-bucket.ts
│ │ ├── middleware.ts
│ │ ├── factory.ts
│ │ ├── config.ts
│ │ ├── types.ts
│ │ └── scripts/
│ │ ├── fixed-window.lua
│ │ ├── sliding-window.lua
│ │ └── token-bucket.lua
│ │
│ ├── redis/
│ │ └── client.ts
│ │
│ ├── middleware/
│ │ └── error-handler.ts
│ │
│ ├── app.ts
│ └── index.ts
│
├── tests/
│ ├── fixed-window.test.ts
│ ├── sliding-window.test.ts
│ ├── token-bucket.test.ts
│ ├── factory.test.ts
│ ├── config.test.ts
│ └── middleware.test.ts
│
├── Dockerfile
├── docker-compose.yml
├── package.json
├── tsconfig.json
├── tsconfig.test.json
├── .env.example
└── README.md
src/rate-limiter/
Contains the core rate-limiting implementation. Each algorithm implements the common RateLimiter interface, allowing the rest of the application to work with any algorithm without depending on its internal implementation.
src/rate-limiter/scripts/
Contains the Redis Lua scripts used by the rate-limiting algorithms. The scripts keep multi-step Redis operations atomic, which is important when multiple requests are processed concurrently.
src/redis/
Contains the Redis client and connection management.
src/middleware/
Contains application-level middleware such as centralized error handling.
src/app.ts
Creates and configures the Express application. The rate limiter is injected into the application instead of being created directly inside it.
src/index.ts
Application entry point. It:
- Loads environment configuration.
- Connects to Redis.
- Creates the rate limiter.
- Creates the Express application.
- Starts the HTTP server.
- Handles graceful shutdown.
tests/
Contains unit and integration tests for the rate-limiting algorithms, configuration, factory, middleware, and error handling.
HTTP Request
│
▼
┌──────────────────────┐
│ Rate Limit Middleware │
└──────────┬────────────┘
│
▼
┌──────────────┐
│ RateLimiter │
│ Interface │
└──────┬───────┘
│
┌──────┴───────┐
│ Factory │
└──────┬───────┘
│
┌──────────────┼──────────────┐
▼ ▼ ▼
Fixed Window Sliding Window Token Bucket
│ │ │
└──────────────┼──────────────┘
▼
Redis
│
▼
Lua Scripts
Limitra uses Vitest for testing and Supertest for testing the Express middleware.
The test suite covers the rate-limiting algorithms, configuration, factory, middleware, error handling, and concurrent requests.
Run all tests once:
npm testRun tests in watch mode:
npm run test:watchThe tests cover:
- Fixed Window rate limiting
- Sliding Window rate limiting
- Token Bucket rate limiting
- Rate-limit configuration validation
- Algorithm factory
- Rate-limit middleware headers (
/api/test/fixed,/api/test/sliding,/api/test/token-bucket) - Response headers
- 429 Too Many Requests responses per algorithm
- Error handling when the limiter fails
- Redis TTL and expiration behavior
- Token refill behavior
- Concurrent requests
Rate limiting needs to behave correctly when multiple requests arrive at the same time.
Limitra includes concurrent request tests that send multiple requests simultaneously to the same limiter.
For example, with a limit of 10 requests, 100 concurrent requests should result in:
Allowed: 10
Rejected: 90
The Redis Lua scripts ensure that the read, decision, and update operations required by each algorithm are executed atomically. This prevents multiple concurrent requests from incorrectly bypassing the configured limit.
Redis is well suited for rate limiting because it provides fast in-memory operations and supports data structures that map naturally to different rate-limiting algorithms.
Limitra uses:
- Redis Strings for Fixed Window counters
- Redis Sorted Sets for Sliding Window request timestamps
- Redis Hashes for Token Bucket state
Redis also provides atomic operations that are important when multiple requests are processed concurrently.
Some rate-limiting operations require multiple Redis commands to be executed as one logical operation.
For example, a Token Bucket request may need to:
- Read the current token count.
- Calculate newly available tokens.
- Check whether the request can be allowed.
- Update the token count.
- Store the latest refill timestamp.
Executing these operations separately could allow concurrent requests to interfere with each other. Lua scripts allow these operations to execute atomically inside Redis.
All three algorithms solve the same problem but use different internal implementations. The common interface provides a consistent contract:
interface RateLimiter {
check(ip: string): Promise<RateLimitResult>
}The middleware only needs to know whether a request is allowed and the associated rate-limit information. It does not need to know which algorithm is being used. This makes the algorithms interchangeable without changing the middleware.
The factory is responsible for selecting the rate-limiter implementation based on the configured algorithm.
Configuration
│
▼
Factory
│
├── Fixed Window
├── Sliding Window
└── Token Bucket
This keeps algorithm selection separate from the Express application and makes the application easier to configure.
The Express application receives a RateLimiter instead of creating one internally.
const limiter = createRateLimiter(config)
const app = createApp(limiter)This keeps the application independent from a specific rate-limiting implementation and makes the middleware easier to test with different limiter implementations.
Each algorithm has different characteristics.
| Algorithm | Main Advantage | Main Trade-off |
|---|---|---|
| Fixed Window | Simple and efficient | Can allow bursts around window boundaries |
| Sliding Window | More precise request tracking | Requires storing individual request timestamps |
| Token Bucket | Supports controlled bursts and gradual refilling | Requires maintaining token state and refill calculations |
The choice of algorithm depends on the requirements of the application.
The current implementation has some intentional limitations:
- Rate limiting is based on client IP addresses.
- The project is designed for Node.js and Express.
- Redis is required for shared rate-limit state.
- The project is provided as source code rather than an npm package.
- The example application exposes a simple test endpoint rather than a complete production API.
Possible improvements for future versions include:
- Support for additional rate-limiting strategies
- More configurable middleware options
- Support for additional web frameworks
- More detailed metrics and observability
- Distributed deployment improvements
- Publishing the project as an npm package
This project is licensed under the MIT License.