ProvenPin Install

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

Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.