npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@sanskari27/aws-rate-limiter

v1.0.0

Published

Production-grade multi-dimensional rate limiter for AWS Lambda and EC2/ECS

Downloads

9

Readme

aws-rate-limiter

Production-grade, multi-dimensional sliding-window rate limiter for Node.js — built for AWS Lambda, EC2, and ECS. Backed by Redis/ElastiCache with atomic Lua scripts, an in-process token reservoir, circuit breaker, and first-class adapters for Express, Fastify, and Lambda.


Table of Contents


How It Works

Every incoming request is checked against up to four dimensions in order — the first one to exceed its limit denies the request immediately (fail-fast):

Request → [1] Per-IP → [2] Per-route → [3] Per-user → [4] Per-user+route → Allowed

Each dimension uses a sliding window counter stored in Redis via atomic Lua scripts (EVALSHA). An optional in-process token reservoir pre-fetches tokens in batches, reducing Redis round-trips by up to 100×. A circuit breaker isolates your app from Redis failures, falling back to configurable policies (open / closed / local).

Your App
  │
  ├── Express / Fastify middleware ──┐
  │                                  ▼
  ├── Lambda Extension + decorator  Rate Limiter Core
  │                                  │
  └── Direct RateLimiter API ────────┤
                                     │   ┌──────────────────┐
                                     ├──▶│ Local Reservoir   │ (in-process, ~0ms)
                                     │   └──────────────────┘
                                     │   ┌──────────────────┐
                                     └──▶│ Redis EVALSHA     │ (~0.5ms same-AZ)
                                         └──────────────────┘

Prerequisites

| Requirement | Version | Notes | |-------------|---------|-------| | Node.js | ≥ 18.x | Required | | Redis | ≥ 6.x | Single node or cluster | | TypeScript | ≥ 5.x | Optional, types included |

Redis options:

  • Local development — Docker: docker run -d -p 6379:6379 redis:7-alpine
  • AWS production — ElastiCache for Redis (single node or cluster mode)
  • Cluster mode — Supported natively; pass redis.cluster instead of redis.url

Installation

npm install aws-rate-limiter

No peer dependencies required. Express, Fastify, Koa, etc. are optional — only install what your project already uses.


Quick Start

import { RateLimiter } from 'aws-rate-limiter';

const limiter = new RateLimiter({
  redis: { url: 'redis://localhost:6379' },
  rules: [
    {
      name: 'default',
      limits: {
        ip:   { limit: 100, window: 60 },  // 100 requests / IP / minute
        user: { limit: 200, window: 60 },  // 200 requests / user / minute
      },
    },
  ],
});

await limiter.connect();

const result = await limiter.check({
  ip:     '1.2.3.4',
  userId: 'user-123',
  route:  '/api/users',
  method: 'GET',
});

if (!result.allowed) {
  // 429 Too Many Requests
  console.log(`Retry after ${result.retryAfter}ms`);
}

await limiter.shutdown();

Configuration

Programmatic Config

Pass a RateLimiterConfig object directly to the constructor:

import { RateLimiter, RateLimiterConfig } from 'aws-rate-limiter';

const config: RateLimiterConfig = {
  // ── Redis connection (required) ───────────────────────────────────────
  redis: {
    url: 'redis://localhost:6379',   // Single node
    // or for cluster:
    // cluster: { nodes: [{ host: 'node1', port: 6379 }, { host: 'node2', port: 6379 }] }
    password: process.env.REDIS_AUTH,
    connectTimeout: 500,             // ms — default: 200
    commandTimeout: 200,             // ms — default: 100
    maxRetriesPerRequest: 2,         // default: 1
  },

  // ── Rules (required, at least one) ────────────────────────────────────
  rules: [
    {
      name: 'default',
      limits: {
        ip:        { limit: 100,  window: 60 },
        route:     { limit: 1000, window: 60 },
        user:      { limit: 200,  window: 60 },
        userRoute: { limit: 50,   window: 60 },
      },
    },
  ],

  // ── Reservoir (optional — improves performance) ────────────────────────
  reservoir: {
    enabled:      true,
    batchSize:    10,    // tokens to pre-fetch per batch
    syncInterval: 1000,  // ms between background syncs
  },

  // ── Failure policy (optional) ──────────────────────────────────────────
  failure: {
    default: 'open',   // 'open' | 'closed' | 'local'
    circuitBreaker: {
      enabled:         true,
      threshold:       5,      // consecutive failures before opening
      recoveryTimeout: 30000,  // ms before attempting recovery
    },
  },

  // ── Observability (optional) ───────────────────────────────────────────
  observability: {
    logLevel:      'info',   // 'debug' | 'info' | 'warn' | 'error'
    logSampleRate: 0.1,      // fraction of allowed requests to log
    metrics:       'none',   // 'none' | 'memory' | 'cloudwatch'
    namespace:     'MyApp',  // CloudWatch/Prometheus namespace
  },
};

const limiter = new RateLimiter(config);

YAML File Config

Create a rate-limiter.yaml file (supports ${ENV_VAR} substitution):

redis:
  url: ${REDIS_URL}
  password: ${REDIS_AUTH}
  connectTimeout: 500
  commandTimeout: 200

rules:
  - name: auth-endpoints
    match:
      routes:
        - "POST /auth/*"
        - "POST /login"
    limits:
      ip:
        limit: 10
        window: 60
    failure: closed

  - name: default
    limits:
      ip:
        limit: 100
        window: 60
      user:
        limit: 200
        window: 60

failure:
  default: open
  circuitBreaker:
    enabled: true
    threshold: 5
    recoveryTimeout: 30000

Load it in your application:

import { loadConfig } from 'aws-rate-limiter/config';

const config = loadConfig('./rate-limiter.yaml');
const limiter = new RateLimiter(config);

loadConfig(path?) resolution order:

  1. Explicit path argument
  2. RATE_LIMITER_CONFIG environment variable
  3. Falls back to loadConfigFromEnv() if no file is found

Environment Variable Config

For simple deployments without a YAML file:

| Variable | Config Field | Default | |----------|-------------|---------| | RATE_LIMITER_REDIS_URL | redis.url | redis://localhost:6379 | | RATE_LIMITER_REDIS_AUTH | redis.password | — | | RATE_LIMITER_DEFAULT_LIMIT | default rule ip.limit | 60 | | RATE_LIMITER_DEFAULT_WINDOW | default rule ip.window (seconds) | 60 | | RATE_LIMITER_FAILURE_POLICY | failure.default | open | | RATE_LIMITER_RESERVOIR_ENABLED | reservoir.enabled | false | | RATE_LIMITER_RESERVOIR_BATCH_SIZE | reservoir.batchSize | 10 | | RATE_LIMITER_RESERVOIR_SYNC_INTERVAL | reservoir.syncInterval (ms) | 1000 | | RATE_LIMITER_CIRCUIT_BREAKER_ENABLED | circuit breaker on/off | false | | RATE_LIMITER_LOG_LEVEL | observability.logLevel | info | | RATE_LIMITER_LOG_SAMPLE_RATE | observability.logSampleRate | 1 | | RATE_LIMITER_METRICS_BACKEND | observability.metrics | none | | RATE_LIMITER_METRICS_NAMESPACE | observability.namespace | — |

RATE_LIMITER_REDIS_URL=redis://my-elasticache:6379 \
RATE_LIMITER_DEFAULT_LIMIT=100 \
RATE_LIMITER_DEFAULT_WINDOW=60 \
node dist/server.js

Then load it:

import { loadConfigFromEnv } from 'aws-rate-limiter/config';

const limiter = new RateLimiter(loadConfigFromEnv());

Framework Adapters

Express

Built-in adapter — no additional dependencies:

import express from 'express';
import { RateLimiter } from 'aws-rate-limiter';
import { createExpressMiddleware } from 'aws-rate-limiter/adapters/express';

const limiter = new RateLimiter({ /* config */ });
await limiter.connect();

const app = express();
app.set('trust proxy', 1);

app.use(
  createExpressMiddleware({
    rateLimiter: limiter,

    // Routes to skip entirely (minimatch globs)
    skipRoutes: ['/health', '/metrics', '/internal/*'],

    // Whether to attach X-RateLimit-* headers on allowed responses (default: true)
    setHeaders: true,

    // How to extract the real client IP
    ipExtraction: {
      trustXForwardedFor: true,
      trustedProxyCount: 1,
    },

    // Return the user's tier for rule matching (e.g. 'free', 'premium', 'enterprise')
    // Without this, rules with match.userTiers will never match.
    getUserTier: (req) => {
      // Option A: header injected by API gateway / auth proxy
      const tier = req.headers['x-user-tier'];
      if (tier) return tier as string;

      // Option B: from decoded JWT attached by earlier auth middleware
      return (req as any).user?.tier;
    },
  }),
);

User identity is extracted automatically from:

  • Authorization: Bearer <token> header → treated as apiKey
  • X-API-Key header → treated as apiKey

To use userId instead, pass userExtraction.apiKeyHeader or populate req.user and provide a custom getUserTier.

Fastify

Built-in adapter — registers as a preHandler hook:

import Fastify from 'fastify';
import { RateLimiter } from 'aws-rate-limiter';
import { createFastifyHook } from 'aws-rate-limiter/adapters/fastify';

const limiter = new RateLimiter({ /* config */ });
await limiter.connect();

const fastify = Fastify({ trustProxy: true });

fastify.addHook(
  'preHandler',
  createFastifyHook({
    rateLimiter: limiter,
    skipRoutes: ['/health', '/metrics'],
    ipExtraction: { trustXForwardedFor: true, trustedProxyCount: 1 },
    getUserTier: (req) => req.headers['x-user-tier'] as string | undefined,
  }),
);

Rate limit headers (X-RateLimit-*) are always set — even on denied requests — so clients can adapt.

AWS Lambda

Lambda uses a two-component pattern to keep a persistent Redis connection alive across invocations:

1. Extension (Lambda Layer) — extension.ts

import { LambdaExtension } from 'aws-rate-limiter/adapters/lambda';

const extension = new LambdaExtension({
  rateLimiterConfig: {
    redis: { url: process.env.REDIS_URL! },
    rules: [
      {
        name: 'default',
        limits: { ip: { limit: 100, window: 60 }, user: { limit: 200, window: 60 } },
      },
    ],
  },
  port: 2772, // default — must match extensionUrl in decorator
});

// Blocks until Lambda sends SHUTDOWN event
await extension.run();

2. Handler Decorator — handler.ts

import { withRateLimit } from 'aws-rate-limiter/adapters/lambda';

export const handler = withRateLimit(
  async (event, context) => {
    return { statusCode: 200, body: JSON.stringify({ message: 'Hello' }) };
  },
  {
    extensionUrl:      'http://localhost:2772', // default
    userIdHeader:      'x-user-id',
    apiKeyHeader:      'x-api-key',
    trustedProxyCount: 1,
    // Cloudfront or other custom IP headers
    ipHeaders: ['cf-connecting-ip', 'true-client-ip'],
  },
);

Why Lambda needs an Extension: Without it, each Lambda invocation opens a new Redis connection (+200ms TLS handshake). The Extension keeps one persistent connection per container — warm invocations have zero connection overhead.

Fail-open by design: If the Extension is unreachable (e.g., still starting up), withRateLimit allows the request through rather than dropping traffic.

Koa

No built-in adapter — use the core RateLimiter API directly as middleware:

import Koa from 'koa';
import { RateLimiter } from 'aws-rate-limiter';

const limiter = new RateLimiter({ /* config */ });
await limiter.connect();

const app = new Koa();
app.proxy = true;

app.use(async (ctx, next) => {
  if (ctx.path === '/health') return next();

  const result = await limiter.check({
    ip:       ctx.ip,
    apiKey:   ctx.get('x-api-key') || undefined,
    route:    ctx.path,
    method:   ctx.method,
    userTier: ctx.get('x-user-tier') || undefined,
  });

  const windowSecs = result.windowSecs ?? 60;
  ctx.set('X-RateLimit-Limit',     String(result.limit));
  ctx.set('X-RateLimit-Remaining', String(Math.max(0, result.remaining)));
  ctx.set('X-RateLimit-Reset',     String(Math.ceil(result.resetAt / 1000)));
  ctx.set('X-RateLimit-Policy',    `${result.limit};w=${windowSecs}`);

  if (!result.allowed) {
    if (result.retryAfter !== undefined) {
      ctx.set('Retry-After', String(Math.ceil(result.retryAfter / 1000)));
    }
    ctx.status = 429;
    ctx.body = { error: 'Too Many Requests', retryAfter: result.retryAfter };
    return;
  }

  await next();
});

NestJS

Two patterns — choose based on your needs:

Pattern 1: Global middleware (simplest — wraps the Express adapter):

import { Injectable, NestMiddleware, Module } from '@nestjs/common';
import { createExpressMiddleware } from 'aws-rate-limiter/adapters/express';
import type { Request, Response, NextFunction } from 'express';

@Injectable()
export class RateLimitMiddleware implements NestMiddleware {
  private readonly middleware = createExpressMiddleware({
    rateLimiter: globalLimiterInstance,
    skipRoutes: ['/health'],
  });

  async use(req: Request, res: Response, next: NextFunction) {
    await this.middleware(req as any, res as any, next);
  }
}

@Module({})
export class AppModule {
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(RateLimitMiddleware).forRoutes('*');
  }
}

Pattern 2: Guard (per-controller or per-route with @UseGuards):

import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common';
import { RateLimiter } from 'aws-rate-limiter';

@Injectable()
export class RateLimitGuard implements CanActivate {
  constructor(private readonly limiter: RateLimiter) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const req  = context.switchToHttp().getRequest();
    const res  = context.switchToHttp().getResponse();
    const result = await this.limiter.check({
      ip:     req.ip,
      apiKey: req.headers['x-api-key'],
      route:  req.path,
      method: req.method,
    });

    res.set('X-RateLimit-Remaining', String(Math.max(0, result.remaining)));

    if (!result.allowed) {
      throw new HttpException('Too Many Requests', HttpStatus.TOO_MANY_REQUESTS);
    }
    return true;
  }
}

Register RateLimiter as a provider with onModuleInit / onModuleDestroy for proper lifecycle management. See examples/nestjs.ts for the full wiring.

Hapi

Use the onPreHandler lifecycle extension:

import Hapi from '@hapi/hapi';
import { RateLimiter } from 'aws-rate-limiter';

const limiter = new RateLimiter({ /* config */ });
await limiter.connect();

const server = Hapi.server({ port: 3000 });

server.ext('onPreHandler', async (request, h) => {
  if (request.path === '/health') return h.continue;

  const ip = request.headers['x-forwarded-for']?.split(',')[0]?.trim()
    || request.info.remoteAddress;

  const result = await limiter.check({
    ip:     ip || '0.0.0.0',
    apiKey: request.headers['x-api-key'],
    route:  request.path,
    method: request.method.toUpperCase(),
  });

  if (!result.allowed) {
    return h.response({ error: 'Too Many Requests' }).code(429).takeover();
  }

  return h.continue;
});

Plain Node.js HTTP

import * as http from 'http';
import { RateLimiter } from 'aws-rate-limiter';

const limiter = new RateLimiter({ /* config */ });
await limiter.connect();

const server = http.createServer(async (req, res) => {
  const ip = (req.headers['x-forwarded-for'] as string)?.split(',')[0]?.trim()
    || req.socket.remoteAddress
    || '0.0.0.0';

  const result = await limiter.check({
    ip,
    apiKey: req.headers['x-api-key'] as string | undefined,
    route:  new URL(req.url!, 'http://localhost').pathname,
    method: req.method || 'GET',
  });

  res.setHeader('X-RateLimit-Limit',     String(result.limit));
  res.setHeader('X-RateLimit-Remaining', String(Math.max(0, result.remaining)));
  res.setHeader('X-RateLimit-Reset',     String(Math.ceil(result.resetAt / 1000)));

  if (!result.allowed) {
    res.writeHead(429, { 'Content-Type': 'application/json' });
    res.end(JSON.stringify({ error: 'Too Many Requests' }));
    return;
  }

  // ... your handler
});

Rate Limit Rules

Per-Route Limits

Rules are matched top-to-bottom — first match wins. Route patterns are matched against "METHOD /path" using minimatch globs.

const limiter = new RateLimiter({
  redis: { url: 'redis://localhost:6379' },
  rules: [
    // Most specific rules first
    {
      name: 'auth',
      match: { routes: ['POST /auth/*', 'POST /login', 'POST /register'] },
      limits: { ip: { limit: 10, window: 60 } },
      failure: 'closed',   // deny all if Redis is down
    },
    {
      name: 'uploads',
      match: { routes: ['POST /api/uploads', 'PUT /api/files/*'] },
      limits: {
        ip:   { limit: 20, window: 300 },  // 20 uploads per 5 min
        user: { limit: 50, window: 300 },
      },
    },
    {
      name: 'search',
      match: { routes: ['GET /api/search', 'GET /api/search/*'] },
      limits: { ip: { limit: 30, window: 60 }, user: { limit: 60, window: 60 } },
    },
    {
      name: 'read-apis',
      match: { routes: ['GET /api/**'] },       // all GET routes under /api/
      limits: { ip: { limit: 200, window: 60 }, user: { limit: 300, window: 60 } },
    },
    {
      name: 'write-apis',
      match: { routes: ['POST /api/**', 'PUT /api/**', 'PATCH /api/**', 'DELETE /api/**'] },
      limits: {
        ip:        { limit: 50, window: 60 },
        user:      { limit: 100, window: 60 },
        userRoute: { limit: 20, window: 60 },
      },
    },
    // Catch-all must be last — no match criteria
    {
      name: 'default',
      limits: { ip: { limit: 100, window: 60 }, user: { limit: 200, window: 60 } },
    },
  ],
});

Glob pattern reference:

| Pattern | Matches | |---------|---------| | "GET /api/users" | Exact route only | | "GET /api/users/*" | One level deep: /api/users/123 | | "GET /api/**" | Any depth: /api/users/123/posts | | "POST /api/**" | All POST routes under /api/ | | "* /api/search" | Any HTTP method to /api/search |

Per-Tier Limits

Use match.userTiers to apply different limits based on the authenticated user's subscription tier. Your adapter must supply the userTier via the getUserTier callback (Express/Fastify) or by setting ctx.userTier directly (core API). If userTier is undefined, tier rules are skipped.

rules: [
  {
    name: 'enterprise',
    match: { userTiers: ['enterprise'] },
    limits: { user: { limit: 10000, window: 60 }, userRoute: { limit: 1000, window: 60 } },
  },
  {
    name: 'premium',
    match: { userTiers: ['premium'] },
    limits: { user: { limit: 1000, window: 60 }, userRoute: { limit: 100, window: 60 } },
  },
  {
    name: 'default',
    limits: { ip: { limit: 100, window: 60 }, user: { limit: 200, window: 60 } },
  },
],

Supply the tier in your adapter:

// Express
createExpressMiddleware({
  rateLimiter: limiter,
  getUserTier: (req) => (req as any).user?.subscription ?? req.headers['x-user-tier'] as string,
});

// Core API
await limiter.check({
  ip:       '1.2.3.4',
  userId:   'user-123',
  route:    '/api/data',
  method:   'GET',
  userTier: 'premium',   // <-- set this from your auth layer
});

Rule Matching

Match conditions use AND semantics — all specified conditions must pass for a rule to match:

{
  name: 'premium-writes',
  match: {
    routes:    ['POST /api/**', 'PUT /api/**'],  // must be a write route
    userTiers: ['premium', 'enterprise'],        // AND must be premium/enterprise
  },
  limits: { user: { limit: 500, window: 60 }, userRoute: { limit: 50, window: 60 } },
}

A rule with no match field acts as a catch-all and matches every request.


Dimensions

Each rule defines limits per dimension. Only configured dimensions are checked; omitted ones are skipped.

| Dimension | Config Key | Redis Key Pattern | When Active | |-----------|-----------|-------------------|-------------| | Per-IP | limits.ip | rl:v1:ip:{ip}:{bucket} | Always | | Per-route | limits.route | rl:v1:route:{method_path}:{bucket} | Always | | Per-user | limits.user | rl:v1:user:{hash}:{bucket} | When userId or apiKey is present | | Per-user+route | limits.userRoute | rl:v1:user-route:{hash}:{route}:{bucket} | When userId or apiKey is present |

User identifiers (userId, apiKey) are SHA-256 hashed before being stored in Redis keys — they are never stored in plain text.

The window value is in seconds. A window of 60 means a 60-second sliding window (not a fixed 1-minute bucket).


Failure Policies

Configure what happens when Redis is unavailable. Can be set globally and overridden per rule:

| Policy | Behaviour | Use For | |--------|-----------|---------| | open (default) | Allow all traffic — rate limiting is temporarily disabled | Public content, read-heavy APIs | | closed | Deny all traffic with 429 | Auth endpoints, payment routes | | local | In-process fixed-window fallback — rate limiting still works, but per-instance not globally | Medium-criticality APIs |

// Global default + per-rule overrides
{
  failure: { default: 'open' },
  rules: [
    { name: 'auth',     match: { routes: ['POST /auth/*'] }, limits: {...}, failure: 'closed' },
    { name: 'payments', match: { routes: ['POST /pay/*']  }, limits: {...}, failure: 'closed' },
    { name: 'search',   match: { routes: ['GET /search']  }, limits: {...}, failure: 'local'  },
    { name: 'default',  limits: {...} },  // inherits 'open' from global default
  ],
}

Circuit breaker — prevents hammering a struggling Redis:

failure: {
  default: 'open',
  circuitBreaker: {
    enabled:         true,
    threshold:       5,      // open after 5 consecutive failures
    recoveryTimeout: 30000,  // try again after 30 seconds
  },
}

States: closed (normal) → open (failing, apply policy) → half-open (testing recovery) → closed.


Reservoir (Performance)

The local token reservoir pre-fetches tokens from Redis in batches, serving most requests from memory without a Redis round-trip:

Without reservoir: every request → Redis (~0.5ms each)
With reservoir:    ~99% of requests → in-memory (~0.01ms), ~1% → Redis refill
reservoir: {
  enabled:      true,
  batchSize:    10,    // tokens to claim per Redis call
  syncInterval: 1000,  // background sync interval (ms)
}

Sizing trade-off: larger batchSize → fewer Redis calls but allows short bursts above the limit. Rule of thumb: batchSize ≤ limit / (expectedInstances × 10).

On shutdown(), unused tokens are returned to Redis. If shutdown is abrupt, tokens expire via Redis TTL (2× window length).


Response Headers

On every rate-limited response (both allowed and denied):

| Header | Value | Example | |--------|-------|---------| | X-RateLimit-Limit | Configured limit for the matched dimension | 100 | | X-RateLimit-Remaining | Remaining requests in current window | 42 | | X-RateLimit-Reset | Unix timestamp (seconds) when window resets | 1706000060 | | X-RateLimit-Policy | Limit and window in RFC-8941 format | 100;w=60 | | Retry-After | Seconds to wait before retrying (429 only) | 15 |


API Reference

new RateLimiter(config)

const limiter = new RateLimiter(config: RateLimiterConfig);

Throws ConfigurationError if rules is empty or config is invalid.

limiter.connect()

await limiter.connect(): Promise<void>

Connects to Redis and loads Lua scripts. Must be called before any other method. Throws RedisConnectionError on failure.

limiter.check(ctx)

await limiter.check(ctx: RateLimitContext): Promise<RateLimitResult>

Evaluates all active dimensions and returns the result. Consumes quota — use status() for read-only queries.

// RateLimitContext
{
  ip:       string;           // required — raw client IP (IPv4 or IPv6)
  route:    string;           // required — request path e.g. "/api/users"
  method:   string;           // required — HTTP method e.g. "GET"
  userId?:  string;           // optional — authenticated user ID
  apiKey?:  string;           // optional — API key (used if no userId)
  userTier?: string;          // optional — subscription tier for rule matching
  cost?:    number;           // optional — request cost weight (default: 1)
}

// RateLimitResult
{
  allowed:     boolean;       // true = proceed, false = 429
  dimension:   string;        // which dimension denied ("ip" | "route" | "user" | "user-route" | "none")
  limit:       number;        // configured limit for triggering dimension
  remaining:   number;        // remaining quota (clamped to 0)
  resetAt:     number;        // Unix epoch ms when window resets
  windowSecs?: number;        // window duration in seconds
  retryAfter?: number;        // ms to wait before retry (only on deny)
  effective:   number;        // sliding-window effective request count
  source:      'redis' | 'reservoir' | 'local_fallback';
}

limiter.status(ctx)

await limiter.status(ctx: RateLimitContext): Promise<RateLimitResult>

Read-only quota check — does not consume quota. Returns the most-constrained dimension. Useful for dashboards and pre-flight checks.

limiter.reset(dimension, identifier)

await limiter.reset(
  dimension:  'ip' | 'user' | 'route' | 'user-route',
  identifier: string,
): Promise<number>  // number of Redis keys deleted

Deletes rate limit counters for a specific dimension and identifier. Use for admin operations (e.g., unblock a user after a support ticket).

// Reset by IP
await limiter.reset('ip', '1.2.3.4');

// Reset a user (pass raw userId — it will be hashed internally)
await limiter.reset('user', 'user-abc-123');

// Reset a specific user+route combination
await limiter.reset('user-route', 'user-abc-123:GET /api/search');

limiter.isConnected()

limiter.isConnected(): boolean

limiter.shutdown()

await limiter.shutdown(): Promise<void>

Flushes the reservoir and disconnects from Redis. Always call on SIGTERM/SIGINT.


Error Handling

import {
  RateLimiterError,
  RedisConnectionError,
  ConfigurationError,
} from 'aws-rate-limiter';

try {
  await limiter.connect();
} catch (err) {
  if (err instanceof RedisConnectionError) {
    console.error('Cannot reach Redis:', err.message, err.code);
  } else if (err instanceof ConfigurationError) {
    console.error('Bad config:', err.message, err.code);
  }
}

// check() does not throw on Redis failure — it applies the failure policy.
// It only throws ConfigurationError if connect() was not called first.
const result = await limiter.check(ctx);
// result.source === 'local_fallback' if Redis was unavailable

| Error Class | code | When Thrown | |-------------|--------|-------------| | ConfigurationError | CONFIGURATION_ERROR | Invalid config, or check() called before connect() | | RedisConnectionError | REDIS_CONNECTION_ERROR | Connection failure during connect() |


Infrastructure

Local Development

# Start Redis
docker run -d --name redis -p 6379:6379 redis:7-alpine

# Verify
redis-cli ping   # → PONG

AWS ElastiCache (Production)

Single node (dev/staging):

redis: { url: 'rediss://my-cluster.xxxxx.ng.0001.use1.cache.amazonaws.com:6379' }
// rediss:// enables TLS — always use in production

Cluster mode (production):

redis: {
  cluster: {
    nodes: [
      { host: 'my-cluster.xxxxx.clustercfg.use1.cache.amazonaws.com', port: 6379 },
    ],
  },
  password: process.env.REDIS_AUTH_TOKEN,
}

Recommended node types:

| Environment | Node Type | Notes | |-------------|-----------|-------| | Dev / Staging | cache.t3.medium | ~20K ops/s | | Small production | cache.r7g.large | ~100K ops/s | | High traffic | cache.r7g.xlarge | ~200K ops/s | | 1M req/s | 6× cache.r7g.large cluster | ~600K ops/s with reservoir |

Security checklist:

  • Always use TLS (rediss://)
  • Set an AUTH token on the ElastiCache cluster
  • Restrict security groups to port 6379 from your application subnets only
  • Never place ElastiCache in a public subnet

Lambda Deployment

Lambda Layer (Extension)
└── extensions/
    └── rate-limiter-extension  (compiled from extension.ts)

Lambda Function
└── handler.js                  (compiled from handler.ts, wrapped with withRateLimit)

The extension binary must be named to match what you register. Set AWS_LAMBDA_RUNTIME_API is injected automatically by the Lambda runtime.


Examples

Full runnable examples for every supported framework are in the examples/ directory:

| File | Framework | Notes | |------|-----------|-------| | examples/express.ts | Express | Built-in adapter | | examples/fastify.ts | Fastify | Built-in adapter | | examples/aws-lambda.ts | AWS Lambda | Extension + decorator | | examples/koa.ts | Koa 2.x | Core API | | examples/nestjs.ts | NestJS 10.x | Middleware + Guard patterns | | examples/hapi.ts | Hapi 21.x | Core API | | examples/node-http.ts | Node.js http | Core API, no framework | | examples/rate-limiter.yaml | — | Full YAML config reference |

For deep dives into the internals, see the docs/ directory:

| Document | Topic | |----------|-------| | 01-architecture.md | System architecture and component map | | 02-algorithm.md | Sliding window counter math | | 03-redis-design.md | Key schema, TTL, cluster topology | | 06-scale-and-performance.md | 1M req/s strategy, reservoir, benchmarks | | 08-failure-modes.md | Failure taxonomy, circuit breaker, fallbacks | | 11-configuration.md | Full config schema reference |