universal-rate-limit

API Reference

Complete API reference for universal-rate-limit core, stores, and middleware packages.

Core

rateLimit(options?)

Creates a rate limiter function.

import { rateLimit } from 'universal-rate-limit';

const limiter = rateLimit(options);
const result: RateLimitResult = await limiter(request);

Parameters:

OptionTypeDefaultDescription
limitnumber | (req: Request) => number | Promise<number>60Maximum requests per window (or bucket capacity for token-bucket)
algorithmAlgorithmConfig | Algorithmsliding-window (60s)Rate limiting algorithm
costnumber | (req: Request) => number | Promise<number>1Units to consume per request
headers'draft-7' | 'draft-6''draft-7'IETF headers version
legacyHeadersbooleanfalseInclude X-RateLimit-* headers
storeStorenew MemoryStore(...)Storage backend
keyGenerator(req: Request) => string | Promise<string>IP-basedExtract client identifier
skip(req: Request) => boolean | Promise<boolean>undefinedSkip rate limiting
handler(req: Request, result: RateLimitResult) => Response | Promise<Response>undefinedCustom response handler
messagestring | Record<string, unknown> | (req, result) => string | Record<string, unknown>'Too Many Requests'Response body
statusCodenumber429HTTP status code
failOpenbooleanfalseFail open on store errors
prefixstringundefinedKey namespace prefix (see Multiple limiters)

Returns: (request: Request) => Promise<RateLimitResult>


RateLimitResult

The result object returned by the limiter function.

interface RateLimitResult {
    limited: boolean;
    limit: number;
    remaining: number;
    resetTime: Date;
    headers: Record<string, string>;
}
FieldTypeDescription
limitedbooleanWhether the request is rate limited
limitnumberMaximum requests allowed in the window
remainingnumberRequests remaining in the current window
resetTimeDateWhen the current window resets
headersRecord<string, string>IETF rate limit headers to set on the response. Includes Retry-After when limited is true.

buildRateLimitResponse(request, result, options)

Helper used by middleware adapters to build a 429 response.

import { buildRateLimitResponse } from 'universal-rate-limit';

const response = await buildRateLimitResponse(request, result, {
    handler: undefined,
    message: 'Too Many Requests',
    statusCode: 429
});

Parameters:

ParameterTypeDescription
requestRequestThe original request
resultRateLimitResultThe rate limit result
options.handler(req, result) => ResponseCustom response handler (takes priority)
options.messagestring | object | functionResponse body
options.statusCodenumberHTTP status code

Returns: Promise<Response>


extractClientIp(request)

Extract the client IP from a Web Standard Request by checking well-known proxy headers (x-forwarded-for, x-real-ip, cf-connecting-ip, fly-client-ip) in order. Returns the first IP found, or '127.0.0.1' when no header is present.

import { extractClientIp } from 'universal-rate-limit';

// Use as part of a custom keyGenerator:
const limiter = rateLimit({
    keyGenerator: req => `${getUserId(req)}:${extractClientIp(req)}`
});

Also available from all middleware packages (e.g. @universal-rate-limit/nextjs).


IP_HEADERS

The ordered list of proxy/CDN headers checked by extractClientIp:

import { IP_HEADERS } from 'universal-rate-limit';
// ['x-forwarded-for', 'x-real-ip', 'cf-connecting-ip', 'fly-client-ip']

Also available from all middleware packages.


Multiple Limiters

When multiple limiters share a store, use the prefix option to namespace their counters. Without it, limiters sharing the same store will collide on the same key for the same client.

import { rateLimit } from 'universal-rate-limit';
import { RedisStore } from '@universal-rate-limit/redis';

const store = new RedisStore({ sendCommand, prefix: 'rl:' });

const authLimiter = rateLimit({ prefix: 'auth', limit: 30, store });
const apiLimiter = rateLimit({ prefix: 'api', limit: 120, store });

How the key is constructed

The final storage key is built in two steps:

  1. Limiter prefixrateLimit() prepends prefix + : to the result of keyGenerator(request): auth + : + 203.0.113.1auth:203.0.113.1

  2. Store prefix — the store (e.g. RedisStore) prepends its own prefix to the key it receives: rl: + auth:203.0.113.1rl:auth:203.0.113.1

┌──────────────┬───────────────┬─────────────────┐
│ Store prefix │ Limiter prefix│ keyGenerator()  │
│ rl:          │ auth:         │ 203.0.113.1     │
└──────────────┴───────────────┴─────────────────┘
 → final key: rl:auth:203.0.113.1

Either prefix is optional. If only store.prefix is set, the key is rl:203.0.113.1. If only options.prefix is set, the key is auth:203.0.113.1. If neither is set, the key is just the IP.


Stores

MemoryStore

In-memory store implementation backed by a single Map, supporting all built-in algorithms.

import { MemoryStore } from 'universal-rate-limit';

const store = new MemoryStore({ prefix: 'my-app:', cleanupIntervalMs: 30_000 });

Constructor:

ParameterTypeDefaultDescription
options.prefixstringundefinedKey prefix
options.cleanupIntervalMsnumber60_000Background cleanup interval

Methods:

MethodDescription
consume(key, algorithm, limit, cost?)Consume capacity, returns ConsumeResult
peek(key, algorithm, limit)Peek without consuming
unconsume(key, algorithm, limit, cost?)Restore consumed capacity
resetKey(key)Reset a single key
resetAll()Clear all entries
shutdown()Stop the cleanup timer

RedisStore

Redis-backed store from @universal-rate-limit/redis. Uses Lua scripts for atomic operations.

import { RedisStore } from '@universal-rate-limit/redis';

const store = new RedisStore({
    sendCommand: (...args) => redis.call(...args),
    prefix: 'rl:'
});

Constructor:

ParameterTypeDefaultDescription
sendCommandSendCommandFnRequired. Sends a raw Redis command.
prefixstring'rl:'Key prefix for all rate limit keys.

Methods:

MethodDescription
consume(key, algorithm, limit, cost?)Consume capacity, returns Promise<ConsumeResult>
peek(key, algorithm, limit)Peek without consuming
resetKey(key)Reset a single key
resetAll()Clear all prefixed keys via SCAN + DEL

Store Interface

Implement this interface for custom storage backends.

interface Store {
    prefix?: string;
    consume(key: string, algorithm: Algorithm, limit: number, cost?: number): MaybePromise<ConsumeResult>;
    peek?(key: string, algorithm: Algorithm, limit: number): MaybePromise<ConsumeResult | undefined>;
    unconsume?(key: string, algorithm: Algorithm, limit: number, cost?: number): MaybePromise<void>;
    resetKey(key: string): MaybePromise<void>;
    resetAll(): MaybePromise<void>;
    shutdown?(): MaybePromise<void>;
}

interface ConsumeResult {
    limited: boolean;
    remaining: number;
    resetTime: Date;
    retryAfterMs: number;
}

Types

type AlgorithmConfig =
    | { type: 'fixed-window'; windowMs: number }
    | { type: 'sliding-window'; windowMs: number }
    | { type: 'token-bucket'; refillRate: number; refillMs?: number };

type HeadersVersion = 'draft-6' | 'draft-7';

Store Exports (@universal-rate-limit/redis)

import { RedisStore } from '@universal-rate-limit/redis';

RedisStore — Client-agnostic Redis store. Also re-exports Store and ConsumeResult from core.


Middleware

@universal-rate-limit/express

import { expressRateLimit } from '@universal-rate-limit/express';

expressRateLimit(options?) — Returns an Express RequestHandler.

@universal-rate-limit/fastify

import { fastifyRateLimit } from '@universal-rate-limit/fastify';

fastifyRateLimit — Fastify plugin. Register with fastify.register(fastifyRateLimit, options).

@universal-rate-limit/hono

import { honoRateLimit } from '@universal-rate-limit/hono';

honoRateLimit(options?) — Returns a Hono MiddlewareHandler.

@universal-rate-limit/nextjs

import { withRateLimit, nextjsRateLimit } from '@universal-rate-limit/nextjs';
  • withRateLimit(handler, options?) — Wraps a Next.js App Router handler with rate limiting.
  • nextjsRateLimit(options?) — Creates a limiter for Edge Middleware use.

All middleware packages also re-export: RateLimitOptions, RateLimitResult, Store, ConsumeResult, MemoryStore, IP_HEADERS, extractClientIp.

On this page