rate-limiter-flexible in-memory limiters: working versions (tested 2026-10-15)
Use rate-limiter-flexible's RateLimiterMemory to consume points, surface rejections with msBeforeNext and remainingPoints, apply blockDuration, penalty and reward, and guard requests with a framework-agnostic middleware tested offline against fake req/res objects.
passed8 passed · 0 failed2026-10-15 22:58 UTC- Status
- passed
- Last passed
- 2026-10-15 22:58 UTC
- Tests
- 8 / 8
- Language
- TypeScript
- Runtime
- node v22.23.3
pinned versions3
rate-limiter-flexible@11.2.2@types/node@26.6.5tsx@4.23.15
run it
npm install --no-audit --no-fund --ignore-scripts=false npm test
test environment
node v22.23.3 linux 6.17.0-1022-azure x64 runner: github-actions 38820570375
The code
Exactly the files that passed. Your agent can pull them with get_recipe("rate-limiter-flexible-memory") (install), or see them on GitHub.
package.json
{
"name": "rate-limiter-flexible-memory",
"private": true,
"type": "module",
"scripts": {
"test": "node --import tsx --test test/*.test.ts"
},
"dependencies": {
"rate-limiter-flexible": "11.2.2"
},
"devDependencies": {
"@types/node": "26.6.5",
"tsx": "4.23.15"
}
}
src/limiter.ts
import { RateLimiterMemory, type RateLimiterRes } from "rate-limiter-flexible";
export type LimiterOptions = {
points: number;
duration: number;
blockDuration?: number;
};
export type ConsumeResult = {
allowed: boolean;
remainingPoints: number;
msBeforeNext: number;
};
export function createLimiter(options: LimiterOptions): RateLimiterMemory {
return new RateLimiterMemory(options);
}
export function isRateLimiterRes(value: unknown): value is RateLimiterRes {
return (
typeof value === "object" &&
value !== null &&
"msBeforeNext" in value &&
"remainingPoints" in value &&
typeof (value as RateLimiterRes).msBeforeNext === "number" &&
typeof (value as RateLimiterRes).remainingPoints === "number"
);
}
export async function tryConsume(
limiter: RateLimiterMemory,
key: string,
points = 1,
): Promise<ConsumeResult> {
try {
const result = await limiter.consume(key, points);
return {
allowed: true,
remainingPoints: result.remainingPoints,
msBeforeNext: result.msBeforeNext,
};
} catch (error) {
if (!isRateLimiterRes(error)) throw error;
return {
allowed: false,
remainingPoints: error.remainingPoints,
msBeforeNext: error.msBeforeNext,
};
}
}
export async function penalize(
limiter: RateLimiterMemory,
key: string,
points = 1,
): Promise<RateLimiterRes> {
return limiter.penalty(key, points);
}
export async function rewardPoints(
limiter: RateLimiterMemory,
key: string,
points = 1,
): Promise<number> {
const result = await limiter.reward(key, points);
return result.remainingPoints;
}
src/middleware.ts
import type { RateLimiterMemory } from "rate-limiter-flexible";
import { isRateLimiterRes } from "./limiter.ts";
export type RateLimitRequest = {
ip?: string;
socket?: { remoteAddress?: string };
};
export type RateLimitResponse = {
statusCode?: number;
body?: unknown;
setHeader?: (name: string, value: string | number) => void;
status?: (code: number) => unknown;
json?: (payload: unknown) => unknown;
};
export function defaultKeyGenerator(req: RateLimitRequest): string {
return req.ip ?? req.socket?.remoteAddress ?? "unknown";
}
export function rateLimitMiddleware(
limiter: RateLimiterMemory,
options: {
points?: number;
keyGenerator?: (req: RateLimitRequest) => string;
} = {},
) {
const points = options.points ?? 1;
const getKey = options.keyGenerator ?? defaultKeyGenerator;
return async function middleware(
req: RateLimitRequest,
res: RateLimitResponse,
next?: () => void,
): Promise<void> {
const key = getKey(req);
try {
const result = await limiter.consume(key, points);
res.setHeader?.("X-RateLimit-Remaining", result.remainingPoints);
res.setHeader?.("Retry-After", Math.ceil(result.msBeforeNext / 1000));
next?.();
} catch (error) {
if (!isRateLimiterRes(error)) throw error;
const retryAfter = Math.ceil(error.msBeforeNext / 1000);
res.setHeader?.("Retry-After", retryAfter);
res.statusCode = 429;
res.status?.(429);
const payload = { error: "Too Many Requests", retryAfter };
if (typeof res.json === "function") {
res.json(payload);
} else {
res.body = payload;
}
}
};
}
test/limiter.test.ts
import { test } from "node:test";
import assert from "node:assert/strict";
import { RateLimiterMemory } from "rate-limiter-flexible";
import {
createLimiter,
isRateLimiterRes,
penalize,
rewardPoints,
tryConsume,
} from "../src/limiter.ts";
test("consume decrements remaining points", async () => {
const limiter = createLimiter({ points: 3, duration: 60 });
const first = await tryConsume(limiter, "user-1");
assert.equal(first.allowed, true);
assert.equal(first.remainingPoints, 2);
const second = await tryConsume(limiter, "user-1");
assert.equal(second.allowed, true);
assert.equal(second.remainingPoints, 1);
});
test("rejection exposes msBeforeNext and remainingPoints", async () => {
const limiter = createLimiter({ points: 2, duration: 60 });
await tryConsume(limiter, "user-2");
await tryConsume(limiter, "user-2");
const rejected = await tryConsume(limiter, "user-2");
assert.equal(rejected.allowed, false);
assert.equal(rejected.remainingPoints, 0);
assert.ok(rejected.msBeforeNext > 0);
});
test("isRateLimiterRes distinguishes rejections from plain errors", async () => {
const limiter = createLimiter({ points: 1, duration: 60 });
await limiter.consume("user-x");
let caught: unknown;
try {
await limiter.consume("user-x");
} catch (error) {
caught = error;
}
assert.equal(isRateLimiterRes(caught), true);
assert.equal(isRateLimiterRes(new Error("nope")), false);
});
test("blockDuration extends msBeforeNext once exhausted", async () => {
const limiter = createLimiter({ points: 1, duration: 1, blockDuration: 60 });
await tryConsume(limiter, "user-3");
const rejected = await tryConsume(limiter, "user-3");
assert.equal(rejected.allowed, false);
assert.ok(
rejected.msBeforeNext > 50_000,
`expected block duration, received ${rejected.msBeforeNext}`,
);
});
test("penalty consumes more points than a normal consume allows", async () => {
const limiter = new RateLimiterMemory({ points: 5, duration: 60 });
const penaltyResult = await penalize(limiter, "user-4", 5);
assert.equal(penaltyResult.consumedPoints, 5);
assert.equal(penaltyResult.remainingPoints, 0);
const after = await tryConsume(limiter, "user-4");
assert.equal(after.allowed, false);
});
test("reward restores consumed points", async () => {
const limiter = createLimiter({ points: 3, duration: 60 });
await tryConsume(limiter, "user-5");
await tryConsume(limiter, "user-5");
const remaining = await rewardPoints(limiter, "user-5", 2);
assert.equal(remaining, 3);
});
test/middleware.test.ts
import { test } from "node:test";
import assert from "node:assert/strict";
import { RateLimiterMemory } from "rate-limiter-flexible";
import {
rateLimitMiddleware,
type RateLimitRequest,
type RateLimitResponse,
} from "../src/middleware.ts";
function createRes(): RateLimitResponse & {
headers: Record<string, string | number>;
} {
const headers: Record<string, string | number> = {};
return {
headers,
statusCode: undefined,
body: undefined,
setHeader(name: string, value: string | number) {
headers[name] = value;
},
};
}
test("middleware calls next while under the limit", async () => {
const limiter = new RateLimiterMemory({ points: 2, duration: 60 });
const middleware = rateLimitMiddleware(limiter);
const req: RateLimitRequest = { ip: "10.0.0.1" };
const res = createRes();
let nextCalls = 0;
await middleware(req, res, () => {
nextCalls += 1;
});
assert.equal(nextCalls, 1);
assert.equal(res.statusCode, undefined);
assert.equal(res.headers["X-RateLimit-Remaining"], 1);
});
test("middleware responds with 429 when the key is exhausted", async () => {
const limiter = new RateLimiterMemory({ points: 1, duration: 60 });
const middleware = rateLimitMiddleware(limiter);
const req: RateLimitRequest = { socket: { remoteAddress: "10.0.0.2" } };
const res = createRes();
let nextCalls = 0;
await middleware(req, res, () => {
nextCalls += 1;
});
await middleware(req, res, () => {
nextCalls += 1;
});
assert.equal(nextCalls, 1);
assert.equal(res.statusCode, 429);
assert.equal((res.body as { error: string }).error, "Too Many Requests");
assert.ok(Number(res.headers["Retry-After"]) >= 1);
});
Connect to ProvenPin
Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.
terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
~/.cursor/mcp.json
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }.vscode/mcp.json
{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }~/.codeium/windsurf/mcp_config.json
{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }MCP settings
{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.