redis-lua-rate-limiter 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,78 @@
1
+ # redis-lua-rate-limiter
2
+
3
+ Distributed, atomic, token-bucket rate limiter for Node.js powered by Redis Lua scripts.
4
+
5
+ ## Why?
6
+
7
+ Most Express rate limiters break in distributed systems.
8
+ This one keeps the logic inside Redis using Lua for atomic safety.
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ npm i redis-lua-rate-limiter
14
+ ```
15
+
16
+ ## Usage
17
+
18
+ ```ts
19
+ import express from "express";
20
+ import { createRateLimiter } from "redis-lua-rate-limiter";
21
+
22
+ const limiter = await createRateLimiter({
23
+ redisUrl: "redis://localhost:6379",
24
+ default: { capacity: 10, refillRate: 5 },
25
+ headers: true,
26
+ });
27
+
28
+ app.use(limiter.middleware());
29
+ ```
30
+
31
+ ## Features
32
+
33
+ - Token bucket algorithm
34
+ - Redis Lua atomic execution
35
+ - Per API key / IP limiting
36
+ - Per route limits
37
+ - Rate limit headers
38
+ - Works across multiple Node servers
39
+
40
+ ## Benchmark
41
+
42
+ ```
43
+ $ npx autocannon -c 1000 -d 15 http://localhost:3000
44
+ Running 15s test @ http://localhost:3000
45
+ ```
46
+
47
+ Tested with 1000 connections using autocannon
48
+
49
+ ```
50
+ ┌─────────┬────────┬────────┬────────┬────────┬───────────┬──────────┬────────┐
51
+ │ Stat │ 2.5% │ 50% │ 97.5% │ 99% │ Avg │ Stdev
52
+ │ Max │
53
+ ├─────────┼────────┼────────┼────────┼────────┼───────────┼──────────┼────────┤
54
+ │ Latency │ 203 ms │ 214 ms │ 253 ms │ 481 ms │ 220.74 ms │ 41.38 ms │ 759 ms │
55
+ └─────────┴────────┴────────┴────────┴────────┴───────────┴──────────┴────────┘
56
+ ┌───────────┬────────┬────────┬─────────┬─────────┬─────────┬────────┬────────┐
57
+ │ Stat │ 1% │ 2.5% │ 50% │ 97.5% │ Avg │ Stdev │ Min │
58
+ ├───────────┼────────┼────────┼─────────┼─────────┼─────────┼────────┼────────┤
59
+ │ Req/Sec │ 2,739 │ 2,739 │ 4,595 │ 5,003 │ 4,516.4 │ 558.22 │ 2,739 │
60
+ ├───────────┼────────┼────────┼─────────┼─────────┼─────────┼────────┼────────┤
61
+ │ Bytes/Sec │ 898 kB │ 898 kB │ 1.51 MB │ 1.64 MB │ 1.48 MB │ 183 kB │ 897 kB │
62
+ └───────────┴────────┴────────┴─────────┴─────────┴─────────┴────────┴────────┘
63
+ ```
64
+ Req/Bytes counts sampled once per second.
65
+ # of samples: 15
66
+ ```
67
+ 160 2xx responses, 67579 non 2xx responses
68
+ 69k requests in 15.22s, 22.2 MB read
69
+
70
+ ```
71
+
72
+ Shows strict enforcement without race conditions.
73
+
74
+ ## Docker
75
+
76
+ ```bash
77
+ docker compose up
78
+ ```
@@ -0,0 +1,2 @@
1
+ import { Redis } from "ioredis";
2
+ export declare function loadLua(redis: Redis): Promise<unknown>;
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.loadLua = loadLua;
7
+ const fs_1 = __importDefault(require("fs"));
8
+ const path_1 = __importDefault(require("path"));
9
+ async function loadLua(redis) {
10
+ const luaPath = path_1.default.join(__dirname, "..", "..", "lua", "token_bucket.lua");
11
+ const script = fs_1.default.readFileSync(luaPath, "utf8");
12
+ const sha = await redis.script("LOAD", script);
13
+ return sha;
14
+ }
15
+ //# sourceMappingURL=lua.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lua.js","sourceRoot":"","sources":["../../src/core/lua.ts"],"names":[],"mappings":";;;;;AAIA,0BAKC;AATD,4CAAoB;AACpB,gDAAwB;AAGjB,KAAK,UAAU,OAAO,CAAC,KAAY;IACxC,MAAM,OAAO,GAAG,cAAI,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,KAAK,EAAE,kBAAkB,CAAC,CAAC;IAC5E,MAAM,MAAM,GAAG,YAAE,CAAC,YAAY,CAAC,OAAO,EAAE,MAAM,CAAC,CAAC;IAChD,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/C,OAAO,GAAG,CAAC;AACb,CAAC"}
@@ -0,0 +1,2 @@
1
+ import Redis from "ioredis";
2
+ export declare function createRedisClient(redisUrl: string): Redis;
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.createRedisClient = createRedisClient;
7
+ const ioredis_1 = __importDefault(require("ioredis"));
8
+ function createRedisClient(redisUrl) {
9
+ return new ioredis_1.default(redisUrl);
10
+ }
11
+ //# sourceMappingURL=redis.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redis.js","sourceRoot":"","sources":["../../src/core/redis.ts"],"names":[],"mappings":";;;;;AAEA,8CAEC;AAJD,sDAA4B;AAE5B,SAAgB,iBAAiB,CAAC,QAAgB;IAChD,OAAO,IAAI,iBAAK,CAAC,QAAQ,CAAC,CAAC;AAC7B,CAAC"}
@@ -0,0 +1,2 @@
1
+ import { Redis } from "ioredis";
2
+ export declare function executeTokenBucket(redis: Redis, sha: string, key: string, capacity: number, refillRate: number): Promise<boolean>;
@@ -0,0 +1,9 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.executeTokenBucket = executeTokenBucket;
4
+ async function executeTokenBucket(redis, sha, key, capacity, refillRate) {
5
+ const now = Math.floor(Date.now() / 1000);
6
+ const allowed = await redis.evalsha(sha, 1, key, capacity, refillRate, now, 1);
7
+ return allowed === 1;
8
+ }
9
+ //# sourceMappingURL=tokenBucket.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"tokenBucket.js","sourceRoot":"","sources":["../../src/core/tokenBucket.ts"],"names":[],"mappings":";;AAEA,gDAoBC;AApBM,KAAK,UAAU,kBAAkB,CACtC,KAAY,EACZ,GAAW,EACX,GAAW,EACX,QAAgB,EAChB,UAAkB;IAElB,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC;IAE1C,MAAM,OAAO,GAAG,MAAM,KAAK,CAAC,OAAO,CACjC,GAAG,EACH,CAAC,EACD,GAAG,EACH,QAAQ,EACR,UAAU,EACV,GAAG,EACH,CAAC,CACF,CAAC;IAEF,OAAO,OAAO,KAAK,CAAC,CAAC;AACvB,CAAC"}
@@ -0,0 +1,5 @@
1
+ import { Request } from "express";
2
+ import { RateLimiterOptions } from "./types";
3
+ export declare function createRateLimiter(options: RateLimiterOptions): Promise<{
4
+ middleware: () => (req: Request, res: any, next: any) => Promise<any>;
5
+ }>;
package/dist/index.js ADDED
@@ -0,0 +1,35 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createRateLimiter = createRateLimiter;
4
+ const redis_1 = require("./core/redis");
5
+ const lua_1 = require("./core/lua");
6
+ async function createRateLimiter(options) {
7
+ const redis = (0, redis_1.createRedisClient)(options.redisUrl);
8
+ const sha = await (0, lua_1.loadLua)(redis);
9
+ const keyGen = options.keyGenerator || ((req) => `rate:${req.ip}`);
10
+ const routes = new Map(Object.entries(options.routes || {}));
11
+ async function rateLimit(key, config) {
12
+ const now = Math.floor(Date.now() / 1000);
13
+ const [allowed, remaining] = (await redis.evalsha(sha, 1, key, config.capacity, config.refillRate, now, 1));
14
+ return { allowed: allowed === 1, remaining };
15
+ }
16
+ function middleware() {
17
+ return async (req, res, next) => {
18
+ const routeConfig = routes.get(req.path) || options.default;
19
+ const key = keyGen(req);
20
+ const { allowed, remaining } = await rateLimit(key, routeConfig);
21
+ if (options.headers) {
22
+ res.setHeader("X-RateLimit-Limit", routeConfig.capacity);
23
+ res.setHeader("X-RateLimit-Remaining", Math.floor(remaining));
24
+ }
25
+ if (!allowed) {
26
+ return res.status(429).json({
27
+ error: "Too Many Requests",
28
+ });
29
+ }
30
+ next();
31
+ };
32
+ }
33
+ return { middleware };
34
+ }
35
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;AAKA,8CA0DC;AA/DD,wCAAiD;AACjD,oCAAqC;AAI9B,KAAK,UAAU,iBAAiB,CAAC,OAA2B;IACjE,MAAM,KAAK,GAAG,IAAA,yBAAiB,EAAC,OAAO,CAAC,QAAQ,CAAC,CAAC;IAClD,MAAM,GAAG,GAAG,MAAM,IAAA,aAAO,EAAC,KAAK,CAAC,CAAC;IAEjC,MAAM,MAAM,GACV,OAAO,CAAC,YAAY,IAAI,CAAC,CAAC,GAAY,EAAE,EAAE,CAAC,QAAQ,GAAG,CAAC,EAAE,EAAE,CAAC,CAAC;IAE/D,MAAM,MAAM,GAAG,IAAI,GAAG,CACpB,MAAM,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,IAAI,EAAE,CAAC,CACrC,CAAC;IAEF,KAAK,UAAU,SAAS,CACtB,GAAW,EACX,MAAoB;QAEpB,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC;QAE1C,MAAM,CAAC,OAAO,EAAE,SAAS,CAAC,GAAG,CAAC,MAAM,KAAK,CAAC,OAAO,CAC/C,GAAa,EACb,CAAC,EACD,GAAG,EACH,MAAM,CAAC,QAAQ,EACf,MAAM,CAAC,UAAU,EACjB,GAAG,EACH,CAAC,CACF,CAAqB,CAAC;QAEvB,OAAO,EAAE,OAAO,EAAE,OAAO,KAAK,CAAC,EAAE,SAAS,EAAE,CAAC;IAC/C,CAAC;IAED,SAAS,UAAU;QACjB,OAAO,KAAK,EAAE,GAAY,EAAE,GAAQ,EAAE,IAAS,EAAE,EAAE;YACjD,MAAM,WAAW,GACf,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,OAAO,CAAC,OAAO,CAAC;YAE1C,MAAM,GAAG,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;YAExB,MAAM,EAAE,OAAO,EAAE,SAAS,EAAE,GAAG,MAAM,SAAS,CAC5C,GAAG,EACH,WAAW,CACZ,CAAC;YAEF,IAAI,OAAO,CAAC,OAAO,EAAE,CAAC;gBACpB,GAAG,CAAC,SAAS,CAAC,mBAAmB,EAAE,WAAW,CAAC,QAAQ,CAAC,CAAC;gBACzD,GAAG,CAAC,SAAS,CAAC,uBAAuB,EAAE,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC;YAChE,CAAC;YAED,IAAI,CAAC,OAAO,EAAE,CAAC;gBACb,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;oBAC1B,KAAK,EAAE,mBAAmB;iBAC3B,CAAC,CAAC;YACL,CAAC;YAED,IAAI,EAAE,CAAC;QACT,CAAC,CAAC;IACJ,CAAC;IAED,OAAO,EAAE,UAAU,EAAE,CAAC;AACxB,CAAC"}
@@ -0,0 +1,2 @@
1
+ import { Request, Response, NextFunction } from "express";
2
+ export declare function createExpressMiddleware(rateLimitFn: (key: string) => Promise<boolean>): (req: Request, res: Response, next: NextFunction) => Promise<Response<any, Record<string, any>> | undefined>;
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createExpressMiddleware = createExpressMiddleware;
4
+ function createExpressMiddleware(rateLimitFn) {
5
+ return async (req, res, next) => {
6
+ const key = `rate:${req.ip}`;
7
+ const allowed = await rateLimitFn(key);
8
+ if (!allowed) {
9
+ return res.status(429).json({ error: "Too Many Requests" });
10
+ }
11
+ next();
12
+ };
13
+ }
14
+ //# sourceMappingURL=express.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"express.js","sourceRoot":"","sources":["../../src/middleware/express.ts"],"names":[],"mappings":";;AAEA,0DAYC;AAZD,SAAgB,uBAAuB,CAAC,WAA8C;IACpF,OAAO,KAAK,EAAE,GAAY,EAAE,GAAa,EAAE,IAAkB,EAAE,EAAE;QAC/D,MAAM,GAAG,GAAG,QAAQ,GAAG,CAAC,EAAE,EAAE,CAAC;QAE7B,MAAM,OAAO,GAAG,MAAM,WAAW,CAAC,GAAG,CAAC,CAAC;QAEvC,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,OAAO,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,mBAAmB,EAAE,CAAC,CAAC;QAC9D,CAAC;QAED,IAAI,EAAE,CAAC;IACT,CAAC,CAAC;AACJ,CAAC"}
@@ -0,0 +1,12 @@
1
+ import { Request } from "express";
2
+ export type BucketConfig = {
3
+ capacity: number;
4
+ refillRate: number;
5
+ };
6
+ export type RateLimiterOptions = {
7
+ redisUrl: string;
8
+ default: BucketConfig;
9
+ routes?: Record<string, BucketConfig>;
10
+ keyGenerator?: (req: Request) => string;
11
+ headers?: boolean;
12
+ };
package/dist/types.js ADDED
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
@@ -0,0 +1,30 @@
1
+ local key = KEYS[1]
2
+
3
+ local capacity = tonumber(ARGV[1])
4
+ local refill_rate = tonumber(ARGV[2])
5
+ local now = tonumber(ARGV[3])
6
+ local requested = tonumber(ARGV[4])
7
+
8
+ local data = redis.call("HMGET", key, "tokens", "timestamp")
9
+ local tokens = tonumber(data[1])
10
+ local timestamp = tonumber(data[2])
11
+
12
+ if tokens == nil then
13
+ tokens = capacity
14
+ timestamp = now
15
+ end
16
+
17
+ local delta = math.max(0, now - timestamp)
18
+ local refill = delta * refill_rate
19
+ tokens = math.min(capacity, tokens + refill)
20
+
21
+ local allowed = tokens >= requested
22
+
23
+ if allowed then
24
+ tokens = tokens - requested
25
+ end
26
+
27
+ redis.call("HMSET", key, "tokens", tokens, "timestamp", now)
28
+ redis.call("EXPIRE", key, 3600)
29
+
30
+ return { allowed and 1 or 0, tokens }
package/package.json ADDED
@@ -0,0 +1,36 @@
1
+ {
2
+ "name": "redis-lua-rate-limiter",
3
+ "version": "1.0.0",
4
+ "description": "Distributed token-bucket rate limiter using Redis Lua scripts for Node.js/Express",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "files": ["dist", "lua"],
8
+
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "dev": "ts-node-dev example/express-app.ts"
12
+ },
13
+ "keywords": [
14
+ "rate-limiter",
15
+ "redis",
16
+ "lua",
17
+ "token-bucket",
18
+ "express",
19
+ "distributed"
20
+ ],
21
+ "author": "shubhankar kumar singh",
22
+ "license": "MIT",
23
+ "dependencies": {
24
+
25
+ "ioredis": "^5.9.2"
26
+ },
27
+ "peerDependencies": {
28
+ "express": "^4 || ^5"
29
+ },
30
+ "devDependencies": {
31
+ "@types/express": "^5.0.6",
32
+ "@types/node": "^25.2.1",
33
+ "ts-node-dev": "^2.0.0",
34
+ "typescript": "^5.9.3"
35
+ }
36
+ }