distrilimit
v1.1.0
Published
Distrilimit is a TypeScript rate-limiting middleware for Express. It exposes a simple `rateLimit(options)` API and lets you choose the storage backend.
Readme
Distrilimit
Distrilimit is a TypeScript rate-limiting middleware for Express. It exposes a simple rateLimit(options) API and lets you choose the storage backend.
Badges
Install
npm install distrilimitWhat You Get
Distrilimit currently supports these strategies:
token-bucketsliding-window-logsliding-window-counterfixed-winodwleaky-bucket
Store choices:
MemoryStoreas the default when no store is passedRedisStorefor production or shared limits
Package Imports
If you want to import the helpers directly, use this public API:
export { RedisStore } from "./store/RedisStore";
export { MemoryStore } from "./store/MemoryStore";
export { rateLimit } from "./rateLimit";Then you can do:
import {
rateLimit,
RedisStore,
MemoryStore,
} from "distrilimit";Quick Start
The most common usage is to install the package and use it directly in an Express app.
import express from "express";
import { rateLimit } from "distrilimit";
const app = express();
app.use(
rateLimit({
strategy: "token-bucket",
capacity: 10,
refillRatePerSecond: 2,
})
);
app.get("/profile", (req, res) => {
res.send("ok");
});
app.listen(3000, () => {
console.log("Server listening on 3000");
});If you do not pass a store, Distrilimit uses MemoryStore automatically.
How To Use It
1. Default MemoryStore
Use this for local development, testing, or single-process apps.
import express from "express";
import { rateLimit } from "distrilimit";
const app = express();
app.use(
rateLimit({
strategy: "leaky-bucket",
capacity: 5,
leakRatePerSecond: 1,
})
);
app.listen(3000);2. RedisStore For Production
Use this when you want shared limits across multiple app instances.
import express from "express";
import { rateLimit, RedisStore } from "distrilimit";
const app = express();
const store = new RedisStore("127.0.0.1", 6379);
app.use(
rateLimit({
strategy: "leaky-bucket",
capacity: 5,
leakRatePerSecond: 1,
store,
})
);
app.listen(3000);3. Fixed Window
Use the current fixed-window strategy literal from the codebase.
import express from "express";
import { rateLimit } from "distrilimit";
const app = express();
app.use(
rateLimit({
strategy: "fixed-winodw",
capacity: 100,
windowSizeMs: 60_000,
})
);
app.listen(3000);4. Sliding Window Counter
import express from "express";
import { rateLimit } from "distrilimit";
const app = express();
app.use(
rateLimit({
strategy: "sliding-window-counter",
capacity: 100,
windowSizeMs: 60_000,
})
);
app.listen(3000);Strategy Reference
Token Bucket
Best when you want burst tolerance.
Options:
capacity: maximum tokens in the bucketrefillRatePerSecond: tokens added every second
Sliding Window Log
Best when you want exact per-window tracking.
Options:
capacity: max requests in the windowwindowSizeMs: size of the moving window
Sliding Window Counter
Best when you want a cheaper approximation than the log.
Options:
capacity: max requests in the windowwindowSizeMs: size of the moving window
Fixed Window
Best when you want simple rate limiting behavior.
Options:
capacity: max requests in the windowwindowSizeMs: size of the window
Leaky Bucket
Best when you want smooth output with controlled draining.
Options:
capacity: max queue/bucket sizeleakRatePerSecond: leak rate in requests per second
Response Shape
The middleware sets standard rate-limit headers:
RateLimit-LimitRateLimit-RemainingRateLimit-ResetRetry-After
If the request is blocked, the response result includes:
{
allowed: boolean;
retryAfterMs: number;
limit: number;
remaining: number;
}Custom Key Generator
By default, Distrilimit identifies clients using req.ip.
You can provide a custom keyGenerator to rate limit by user ID, API key, tenant ID, or any other unique identifier.
Rate limit by authenticated user
app.use(
rateLimit({
strategy: "token-bucket",
capacity: 10,
refillRatePerSecond: 2,
keyGenerator: (req) => req.user.id,
})
);Rate limit by API key
app.use(
rateLimit({
strategy: "token-bucket",
capacity: 20,
refillRatePerSecond: 5,
keyGenerator: (req) =>
req.headers["x-api-key"] as string,
})
);The value returned from keyGenerator is used as the unique identifier for rate limiting.
Custom Block Handler
By default, Distrilimit responds with HTTP 429 (Too Many Requests) when a client exceeds the configured limit.
You can customize this response using the handler option.
app.use(
rateLimit({
strategy: "token-bucket",
capacity: 5,
refillRatePerSecond: 1,
handler: (req, res, result) => {
return res.status(429).json({
success: false,
message: "Rate limit exceeded",
retryAfterMs: result.retryAfterMs,
});
},
})
);The custom handler is invoked only when a request is blocked. If no handler is provided, Distrilimit sends its default 429 Too Many Requests response.
Throughput Benchmark
Example table:
| Algorithm | Time (ms) | Latency (ms/request) | Throughput (req/s) | Allowed | | --- | ---: | ---: | ---: | ---: | | Token Bucket | 369 | 0.369 | 2710.0271 | 1000 | | Sliding Window Log | 358 | 0.358 | 2793.2960 | 1000 | | Sliding Window Counter | 404 | 0.404 | 2475.2475 | 1000 |
Rate-Limit Behavior Benchmark
Example table:
| Algorithm | Requests | Allowed | Rejected | Total Time (ms) | | --- | ---: | ---: | ---: | ---: | | Token Bucket | 1000 | 100 | 900 | xxx | | Sliding Window Log | 1000 | 100 | 900 | 326 | | Sliding Window Counter | 1000 | 100 | 900 | 273 |
app.use(
rateLimit({
strategy: "token-bucket",
capacity: 10,
refillRatePerSecond: 2,
})
);Router-Level Rate Limiting
You can apply Distrilimit to an entire Express router. Every route registered after the middleware will share the configured rate limit.
import express from "express";
import { rateLimit } from "distrilimit";
import { protectRoute } from "./middleware/auth.js";
const router = express.Router();
router.use(protectRoute);
router.use(
rateLimit({
strategy: "token-bucket",
capacity: 10,
refillRatePerSecond: 2,
})
);
router.get("/profile", getProfile);
router.post("/message", sendMessage);
router.get("/notifications", getNotifications);
export default router;This applies the rate limiter to every route within the router.
If you need different limits for different routes, apply separate rateLimit() middleware to those specific routes or create multiple routers with different configurations.
Frontend Integration
When a client exceeds the configured rate limit, Distrilimit responds with HTTP 429 and includes retryAfterMs in the response body. You can use this value to notify users when they should retry.
Example using Axios:
try {
const res = await axiosInstance.get("/messages/unreadmessages");
set({ unreadCounts: res.data.unreadMap });
} catch (error: any) {
if (error.response?.status === 429) {
toast.error(
`Too many requests. Please try again in ${Math.ceil(
error.response.data.retryAfterMs / 1000
)} seconds.`
);
return;
}
toast.error("Failed to load unread messages.");
}Tested in a real-world application (Privex) to verify middleware integration and frontend handling of rate-limited responses.
Project Structure
src/
createRateLimit.ts
index.ts
server.ts
config/
Factory/
middleware/
models/
scripts/
store/
strategies/
types/Contributing
- Clone the repo.
- Install dependencies.
- Run
npm run devfor the example app. - Make a focused change.
- Open a pull request.
License
MIT
