Skip to content

Getting Started

Installation

Install the core package:

bash
npm install universal-rate-limit

Or with your preferred package manager:

bash
pnpm add universal-rate-limit
yarn add universal-rate-limit

If you're using a framework, install the corresponding middleware package instead — it includes the core as a dependency:

bash
npm install @universal-rate-limit/express
npm install @universal-rate-limit/fastify
npm install @universal-rate-limit/hono
npm install @universal-rate-limit/nextjs

For distributed deployments, add the Redis store:

bash
npm install @universal-rate-limit/redis

Basic Usage

The rateLimit function creates a limiter that accepts a Web Standard Request and returns a RateLimitResult:

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

const limiter = rateLimit({
    windowMs: 60_000, // 1 minute window
    limit: 60 // 60 requests per window
});

// In your request handler:
const result = await limiter(request);

if (result.limited) {
    return new Response('Too Many Requests', {
        status: 429,
        headers: result.headers
    });
}

// Continue processing...

Options

All options are optional with sensible defaults:

OptionTypeDefaultDescription
windowMsnumber60_000Time window in milliseconds
limitnumber | (req) => number60Max requests per window (can be async)
algorithm'fixed-window' | 'sliding-window''fixed-window'Rate limiting algorithm
headers'draft-7' | 'draft-6''draft-7'IETF rate limit headers version
storeStoreMemoryStoreStorage backend (Redis, custom)
keyGenerator(req) => stringIP-basedExtract client identifier
skip(req) => booleanundefinedSkip rate limiting for certain requests
handler(req, result) => ResponseundefinedCustom 429 response handler
messagestring | object | function'Too Many Requests'Response body when limited
statusCodenumber429HTTP status code when limited
passOnStoreErrorbooleanfalseFail open if the store errors

Sliding Window

The sliding-window algorithm provides smoother rate limiting by weighting the previous window's hits:

ts
const limiter = rateLimit({
    windowMs: 60_000,
    limit: 100,
    algorithm: 'sliding-window'
});

Custom Key Generator

By default, the client IP is extracted from common proxy headers (x-forwarded-for, x-real-ip, cf-connecting-ip, fly-client-ip). Override this for custom logic:

ts
const limiter = rateLimit({
    keyGenerator: request => {
        // Rate limit by API key
        return request.headers.get('x-api-key') ?? '127.0.0.1';
    }
});

Dynamic Limits

The limit option accepts a function for per-request limits:

ts
const limiter = rateLimit({
    limit: async request => {
        const apiKey = request.headers.get('x-api-key');
        if (apiKey === 'premium') return 1000;
        return 60;
    }
});

Skip Requests

Bypass rate limiting for certain requests:

ts
const limiter = rateLimit({
    skip: request => {
        return new URL(request.url).pathname === '/health';
    }
});

Custom Response

Customize the 429 response body. A Retry-After header is included automatically on all 429 responses.

ts
const limiter = rateLimit({
    message: { error: 'Rate limit exceeded' },
    statusCode: 429
});

Or use a handler function for full control:

ts
const limiter = rateLimit({
    handler: (request, result) => {
        return new Response(JSON.stringify({ error: 'Too many requests', reset: result.resetTime }), { status: 429, headers: { 'Content-Type': 'application/json' } });
    }
});

Released under the MIT License.