<div>

# Awesome Rate Limiter - Universal Rate Limiting for Node.js APIs

---

`awesome-rate-limiter` is a universal **Node.js rate limiter** built around two main strategies: **fixed-window rate limiting** and **sliding-window rate limiting**. It includes an automatic block system for repeated limit violations and lets you store rate-limit information, request records, block history, and usage data in any database through a custom store or the `onRecord` hook.

Use it to build safer APIs with Express, Fastify, Koa, Hono, and custom Node.js frameworks. It helps protect public routes, login endpoints, authentication flows, REST APIs, and backend services from request abuse, brute-force attacks, traffic spikes, and basic DDoS-style flooding.

The package gives you a production-ready API rate limiting layer with TypeScript support, progressive blocking, response headers, request event callbacks, and pluggable storage. You can start with the built-in memory store for local development, then move to Redis, MongoDB, PostgreSQL, SQLite, DynamoDB, or any custom database adapter for distributed deployments and long-term analytics.

Use `awesome-rate-limiter` when you need an **Express rate limiter**, **Fastify rate limiter**, **Koa rate limiter**, **Hono rate limiter**, or framework-agnostic rate limit core with a consistent configuration style across your entire Node.js application.

---

[![npm version](https://img.shields.io/npm/v/awesome-rate-limiter.svg?style=flat-square)](https://www.npmjs.com/package/awesome-rate-limiter)
[![TypeScript](https://img.shields.io/badge/TypeScript-5.x-3178c6?style=flat-square&logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow?style=flat-square)](https://opensource.org/licenses/MIT)
[![Zero Dependencies](https://img.shields.io/badge/dependencies-zero-brightgreen?style=flat-square)](https://www.npmjs.com/package/awesome-rate-limiter)

</div>

## Table of Contents

- [Features](#features)
- [Framework Support](#framework-support)
- [Installation](#installation)
- [Quick Start](#quick-start)
- [Full Example With All Options](#full-example-with-all-options)
- [Options Reference](#options-reference)
- [Strategies](#strategies)
- [Convenience Factories](#convenience-factories)
- [Storage Backends](#storage-backends)
- [onRecord - Manual Persistence](#onrecord---manual-persistence)
- [Progressive Blocking](#progressive-blocking)
- [Response Headers](#response-headers)
- [Lifecycle Methods](#lifecycle-methods)
- [Event Callbacks](#event-callbacks)
- [TypeScript](#typescript)
- [Custom Framework Adapter](#custom-framework-adapter)
- [Best Practices](#best-practices)
- [Contributors](#contributors)
- [Reporting Issues](#reporting-issues)

---

## Features

|                            |                                                                                  |
| -------------------------- | -------------------------------------------------------------------------------- |
| **Universal**              | Express, Fastify, Koa, Hono - and any custom Node.js framework                   |
| **Two strategies**         | Sliding-window (burst-safe, default) and fixed-window (fast, low memory)         |
| **Progressive blocking**   | Auto-escalating ban durations based on 30-day violation history                  |
| **Four storage backends**  | In-memory, Redis, MongoDB, PostgreSQL - or bring your own                        |
| **`onRecord` hook**        | Full request snapshot after every hit - persist to any DB without a custom store |
| **Full TypeScript safety** | `TReq` generic flows through every callback - typed per framework, zero casting  |
| **Dual CJS / ESM**         | `.d.ts` declarations included, works in all modern bundlers                      |
| **Zero runtime deps**      | Core has no dependencies; adapters only require the framework you choose         |

---

## Framework Support

| Framework     | Import                          | Mounting                                              |
| ------------- | ------------------------------- | ----------------------------------------------------- |
| **Express**   | `awesome-rate-limiter/express` | `app.use(limiter)`                                    |
| **Fastify**   | `awesome-rate-limiter/fastify` | `app.addHook('onRequest', limiter)`                   |
| **Koa**       | `awesome-rate-limiter/koa`     | `app.use(limiter)`                                    |
| **Hono**      | `awesome-rate-limiter/hono`    | `app.use('*', limiter)`                               |
| **NestJS**    | `awesome-rate-limiter/express` | NestJS runs on Express by default                     |
| **Any other** | `awesome-rate-limiter` (core)  | [Build a 5-method adapter](#custom-framework-adapter) |

---

## Installation

```bash
npm install awesome-rate-limiter
```

---

## Quick Start

`createRateLimiter({...})` is **identical across all frameworks** - only the import path and mount call differ.

```typescript
// Change only this line to switch frameworks:
import { createRateLimiter } from 'awesome-rate-limiter/express' // or /fastify /koa /hono

const limiter = createRateLimiter({ windowMs: 60_000, maxRequests: 100 })
```

| Framework           | Mount                                               |
| ------------------- | --------------------------------------------------- |
| Express / Koa       | `app.use(limiter)`                                  |
| Fastify             | `app.addHook('onRequest', limiter)`                 |
| Hono                | `app.use('*', limiter)`                             |
| Fastify - per-route | `app.get('/path', { onRequest: limiter }, handler)` |
| Express - per-route | `app.post('/login', limiter, handler)`              |

**Express example:**

```typescript
import express from 'express'
import { createRateLimiter } from 'awesome-rate-limiter/express'

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

app.use(createRateLimiter({ windowMs: 60_000, maxRequests: 100 }))
app.listen(3000)
```

**Hono (Cloudflare Workers / Bun / Deno):**

```typescript
import { Hono } from 'hono'
import { createRateLimiter } from 'awesome-rate-limiter/hono'

const app = new Hono()
app.use('*', createRateLimiter({ windowMs: 60_000, maxRequests: 100 }))
export default app
```

---

## Full Example With All Options

This example shows how to use `awesome-rate-limiter` with fixed-window or sliding-window rate limiting, automatic blocking, custom keys, skip rules, whitelist rules, headers, dry-run mode, and database persistence through `onRecord`.

```typescript
import express from 'express'
import { createRateLimiter } from 'awesome-rate-limiter/express'
import { MemoryStore } from 'awesome-rate-limiter/stores'

const app = express()

// Replace this with Prisma, Mongoose, MongoDB, PostgreSQL, Redis, or any DB client.
const db = {
  rateLimitRecords: {
    upsert: async (_record: unknown) => {}
  }
}

// Required when your app is behind a proxy, load balancer, Nginx, or Vercel/Render/Fly.io.
// It helps Express read the real client IP correctly.
app.set('trust proxy', 1)

const limiter = createRateLimiter({
  // Rate-limiting algorithm.
  // Use 'sliding' for more accurate burst protection.
  // Use 'fixed' for simple counters with lower memory usage.
  strategy: 'sliding',

  // Time window in milliseconds.
  // Here: each key can make maxRequests requests every 60 seconds.
  windowMs: 60_000,

  // Maximum allowed requests per key inside the selected window.
  maxRequests: 100,

  // First block duration after a key exceeds the limit.
  // Repeated violations can be blocked for longer by the progressive block system.
  initialBlockMs: 20 * 60_000,

  // Custom 429 response body.
  // You can use a string or an object.
  message: {
    error: 'Too many requests',
    message: 'Please wait before trying again.'
  },

  // Create the unique rate-limit key for each request.
  // Common choices: req.ip, user ID, API key, tenant ID, or route + IP.
  // If your auth layer adds req.user, replace this with req.user.id.
  keyGenerator: req => req.ip || 'unknown-client',

  // Turn on internal logs while debugging rate-limit behavior.
  enableLogger: false,

  // Skip rate limiting for selected requests.
  // Useful for health checks, internal routes, or trusted services.
  skip: req => req.path === '/health',

  // Store current counters and block records.
  // MemoryStore is good for local development and single-process apps.
  // For distributed apps, use Redis, MongoDB, PostgreSQL, or your own custom store.
  store: new MemoryStore({ maxKeys: 50_000 }),

  // Maximum unique keys kept by the default MemoryStore.
  // Ignored when you provide a custom store.
  maxKeys: 50_000,

  // Keys that should never be rate-limited.
  // You can also pass a function: (key, req) => boolean.
  whitelist: ['127.0.0.1', '::1'],

  // Track requests and run callbacks without actually blocking users.
  // Useful before enabling rate limits in production.
  dryRun: false,

  // Send X-RateLimit-* and Retry-After response headers.
  headers: true,

  // Add extra request data to the onRecord payload.
  // Use this when you want to store route, user, API key, or tenant info.
  extractRequestData: req => ({
    method: req.method,
    route: req.originalUrl,
    userAgent: req.headers['user-agent'],
    apiKey: req.headers['x-api-key']
  }),

  // Runs after every request, allowed or blocked.
  // Store this data in any database for analytics, audit logs, dashboards, or abuse tracking.
  onRecord: async record => {
    await db.rateLimitRecords.upsert({
      where: { key: record.key },
      update: {
        count: record.count,
        remaining: record.remaining,
        isBlocked: record.isBlocked,
        blockCount: record.blockCount,
        blockExpiresAt: record.blockExpiresAt,
        resetTime: record.resetTime,
        requestData: record.requestData,
        updatedAt: new Date(record.timestamp)
      },
      create: {
        key: record.key,
        strategy: record.strategy,
        count: record.count,
        remaining: record.remaining,
        isBlocked: record.isBlocked,
        blockCount: record.blockCount,
        blockExpiresAt: record.blockExpiresAt,
        resetTime: record.resetTime,
        requestData: record.requestData,
        createdAt: new Date(record.timestamp),
        updatedAt: new Date(record.timestamp)
      }
    })
  },

  // Runs when a key becomes blocked.
  // Good for security alerts, logs, or admin notifications.
  onBlocked: async event => {
    console.log('Blocked key:', event.key)
    console.log('Block duration:', event.blockDuration)
    console.log('Unblock time:', new Date(event.unblockTime))
  },

  // Runs when a key uses the final allowed request in the window.
  // The next request from this key will be blocked.
  onLimitReached: event => {
    console.log(`${event.key} reached ${event.count}/${event.limit} requests`)
  },

  // Runs for every allowed request.
  // Useful for lightweight metrics or request activity tracking.
  onRequestAllowed: event => {
    console.log(`${event.key} allowed, remaining: ${event.remaining}`)
  }
})

// Apply globally to every route.
app.use(limiter)

// Or apply only to sensitive routes.
app.post('/auth/login', limiter, (req, res) => {
  res.json({ success: true })
})

app.listen(3000)
```

For Fastify, Koa, or Hono, keep the same configuration object and only change the import path and mount style.

```typescript
import { createRateLimiter } from 'awesome-rate-limiter/fastify'
import { createRateLimiter } from 'awesome-rate-limiter/koa'
import { createRateLimiter } from 'awesome-rate-limiter/hono'
```

---

## Options Reference

> All options are identical across every adapter. `TReq` resolves to the framework's request type automatically.

| Option               | Type                                                        | Default       | Description                                               |
| -------------------- | ----------------------------------------------------------- | ------------- | --------------------------------------------------------- |
| `strategy`           | `'sliding' \| 'fixed'`                                      | `'sliding'`   | Rate-limiting algorithm                                   |
| `windowMs`           | `number`                                                    | `60_000`      | Tracking window in milliseconds                           |
| `maxRequests`        | `number`                                                    | `15`          | Max requests per window per key                           |
| `initialBlockMs`     | `number`                                                    | `1_200_000`   | First-offence block duration (20 min)                     |
| `message`            | `string \| object`                                          | built-in      | Custom 429 response body                                  |
| `keyGenerator`       | `(req: TReq) => string`                                     | `extractIp`   | Derive the rate-limit key                                 |
| `skip`               | `(req: TReq) => boolean \| Promise<boolean>`                | none          | Return `true` to bypass rate-limiting                     |
| `whitelist`          | `string[] \| ((key, req) => boolean)`                       | none          | Keys or predicate never rate-limited                      |
| `store`              | `RateLimitStore`                                            | `MemoryStore` | Storage backend                                           |
| `maxKeys`            | `number`                                                    | `10_000`      | LRU cap on MemoryStore (ignored when `store` is provided) |
| `dryRun`             | `boolean`                                                   | `false`       | Track and fire callbacks but never block                  |
| `headers`            | `boolean`                                                   | `true`        | Emit `X-RateLimit-*` and `Retry-After` headers            |
| `enableLogger`       | `boolean`                                                   | `false`       | Log internal activity to stdout                           |
| `onRecord`           | `(data: RecordData<TReq>) => void \| Promise<void>`         | none          | Fires after every request                                 |
| `extractRequestData` | `(req: TReq) => Record<string, unknown>`                    | none          | Attach custom fields to the `onRecord` payload            |
| `onBlocked`          | `(event: BlockedEvent<TReq>) => void \| Promise<void>`      | none          | Fires when a key is newly blocked                         |
| `onLimitReached`     | `(event: LimitReachedEvent<TReq>) => void \| Promise<void>` | none          | Fires when the last slot is consumed                      |
| `onRequestAllowed`   | `(event: AllowedEvent<TReq>) => void \| Promise<void>`      | none          | Fires for every allowed request                           |

---

## Strategies

|                  | Sliding Window _(default)_                  | Fixed Window                             |
| ---------------- | ------------------------------------------- | ---------------------------------------- |
| **How it works** | Window rolls continuously with each request | Counter resets at a hard boundary        |
| **Burst safety** | No boundary bursts possible                 | Up to 2x `maxRequests` burst at boundary |
| **Best for**     | APIs, login, any burst-sensitive route      | Background jobs, internal services       |

```typescript
createRateLimiter({ strategy: 'sliding' }) // default
createRateLimiter({ strategy: 'fixed' })
```

---

## Convenience Factories

Every adapter exports these factories. Import from whichever adapter path you use.

| Factory                                        | Equivalent to                                         |
| ---------------------------------------------- | ----------------------------------------------------- |
| `createFixedRateLimiter(opts)`                 | `createRateLimiter({ strategy: 'fixed', ...opts })`   |
| `createSlidingRateLimiter(opts)`               | `createRateLimiter({ strategy: 'sliding', ...opts })` |
| `createUserRateLimiter(opts)`                  | Sliding + keys by authenticated user ID (IP fallback) |
| `createSlidingUserRateLimiter(opts)`           | Same as above, sliding explicitly                     |
| `createEndpointRateLimiter(path, opts)`        | Per-route counter - key = `"path:ip"`                 |
| `createSlidingEndpointRateLimiter(path, opts)` | Same, sliding window                                  |

**User-scoped key sources by adapter:**

| Adapter                         | User ID source                                         |
| ------------------------------- | ------------------------------------------------------ |
| `awesome-rate-limiter/express` | `req.user.id` or `req.user._id`                        |
| `awesome-rate-limiter/koa`     | `ctx.state.user.id` or `ctx.state.user._id`            |
| `awesome-rate-limiter/hono`    | `c.get('user').id` or `c.get('user')._id`              |
| `awesome-rate-limiter/fastify` | Use `keyGenerator` - no standard `req.user` in Fastify |

```typescript
// Must run auth logic first to populate req.user
app.use(authMiddleware)
app.use(createUserRateLimiter({ maxRequests: 500 }))

// Tight per-route limit
app.post(
  '/auth/login',
  createEndpointRateLimiter('/auth/login', {
    maxRequests: 5,
    windowMs: 900_000
  })
)
```

---

## Storage Backends

### MemoryStore _(default)_

Zero configuration. **Not shared across processes** - use Redis, MongoDB, or PostgreSQL for multi-process deployments.

```typescript
import { MemoryStore } from 'awesome-rate-limiter'
createRateLimiter({ store: new MemoryStore({ maxKeys: 50_000 }) })
```

---

### Redis

Best for high-throughput APIs. Atomic operations, sub-millisecond reads, automatic key expiry via TTL.

```typescript
import Redis from 'ioredis'
import { RedisRateLimitStore } from './stores/RedisRateLimitStore'

const redis = new Redis(process.env.REDIS_URL)
const limiter = createRateLimiter({
  store: new RedisRateLimitStore(redis, 60_000),
  windowMs: 60_000,
  maxRequests: 100
})
```

> Full adapter implementation: `examples/RedisRateLimitStore.ts`

---

### MongoDB

Uses TTL indexes for automatic document expiry - good fit if you already run MongoDB.

```typescript
import { MongoRateLimitStore } from './stores/MongoRateLimitStore'

const limiter = createRateLimiter({
  store: new MongoRateLimitStore(60_000),
  windowMs: 60_000,
  maxRequests: 100
})
```

> Full adapter implementation: `examples/MongoRateLimitStore.ts`

---

### PostgreSQL

Uses `ON CONFLICT DO UPDATE` for atomic upserts. No automatic TTL - expired rows are removed by the built-in `cleanup()` hook (runs every 2 minutes).

```typescript
import { Pool } from 'pg'
import { PostgresRateLimitStore } from './stores/PostgresRateLimitStore'

const pool = new Pool({ connectionString: process.env.DATABASE_URL })
const store = new PostgresRateLimitStore({ pool, windowMs: 60_000 })

await store.initialize() // creates tables if they don't exist

const limiter = createRateLimiter({ store, windowMs: 60_000, maxRequests: 100 })
```

> Full adapter implementation with `tablePrefix` option: `examples/PostgresRateLimitStore.ts`

---

### Custom Store

Implement `RateLimitStore` from `awesome-rate-limiter/stores` to support any backend (DynamoDB, SQLite, Valkey, etc.):

```typescript
import type {
  RateLimitStore,
  StoreEntry,
  BlockRecord
} from 'awesome-rate-limiter/stores'

class MyStore implements RateLimitStore {
  getEntry(key: string): Promise<StoreEntry | null> {
    /* ... */
  }
  setEntry(key: string, e: StoreEntry): Promise<void> {
    /* ... */
  }
  deleteEntry(key: string): Promise<void> {
    /* ... */
  }

  getBlock(key: string): Promise<BlockRecord | null> {
    /* ... */
  }
  setBlock(key: string, r: BlockRecord): Promise<void> {
    /* ... */
  }
  deleteBlock(key: string): Promise<void> {
    /* ... */
  }

  resetKey(key: string): Promise<void> {
    /* ... */
  }
  resetAll(): Promise<void> {
    /* ... */
  }
  entryCount(): Promise<number> {
    /* ... */
  }
  blockedCount(): Promise<number> {
    /* ... */
  }

  // Optional - called every 2 minutes
  cleanup?(windowMs: number): Promise<void> {
    /* ... */
  }
  // Optional - called by limiter.destroy()
  destroy?(): Promise<void> {
    /* ... */
  }
}
```

---

## onRecord - Manual Persistence

Fires after **every** request (allowed and blocked) with a full data snapshot. Use it to log activity without writing a full custom store.

```typescript
createRateLimiter({
  extractRequestData: req => ({ userId: req.user?.id, route: req.path }),

  onRecord: async ({
    key,
    count,
    remaining,
    isBlocked,
    requestData,
    timestamp
  }) => {
    await db
      .collection('rateLog')
      .updateOne(
        { key },
        { $set: { count, remaining, isBlocked, timestamp, ...requestData } },
        { upsert: true }
      )
  }
})
```

**`RecordData<TReq>` shape:**

```typescript
{
  key:             string
  req:             TReq
  strategy:        'fixed' | 'sliding'
  count:           number
  remaining:       number
  resetTime:       number        // epoch ms when window/block resets
  isBlocked:       boolean
  dryRunBlocked:   boolean
  blockCount?:     number
  blockExpiresAt?: number
  timestamp:       number
  requestData?:    Record<string, unknown>
}
```

> Errors thrown inside `onRecord` are caught and logged - they never propagate to the request.

---

## Progressive Blocking

When a key exceeds the limit it is blocked for an escalating duration based on its 30-day violation history:

| Violation history (last 30 days) | Block duration                       |
| -------------------------------- | ------------------------------------ |
| First offence                    | `initialBlockMs` _(default: 20 min)_ |
| 5+ short blocks                  | 1 day                                |
| 3+ day-length blocks             | 7 days                               |
| 3+ week-length blocks            | 30 days                              |
| 3+ month-length blocks           | 1 year                               |

```typescript
createRateLimiter({ initialBlockMs: 5 * 60_000 }) // set first-offence to 5 minutes
```

Blocked requests receive a `429` with a `Retry-After` header.

---

## Response Headers

Set automatically when `headers: true` (default):

| Header                  | Value                                             |
| ----------------------- | ------------------------------------------------- |
| `X-RateLimit-Limit`     | `maxRequests`                                     |
| `X-RateLimit-Remaining` | Slots left                                        |
| `X-RateLimit-Reset`     | Epoch seconds of next reset                       |
| `X-RateLimit-Window`    | Window duration in seconds                        |
| `X-RateLimit-Strategy`  | `fixed` or `sliding`                              |
| `X-RateLimit-DryRun`    | `true` _(only when `dryRun: true`)_               |
| `Retry-After`           | Seconds until unblocked _(blocked requests only)_ |

---

## Lifecycle Methods

Every limiter instance exposes the same API regardless of framework:

```typescript
await limiter.getInfo('203.0.113.5')
// → { key, strategy, count, remaining, resetTime, isBlocked, blockExpiresAt?, blockCount? }

await limiter.getStats()
// → { strategy, trackedKeys, blockedKeys, windowMs, maxRequests }

await limiter.reset('203.0.113.5') // unblock / clear one key
await limiter.resetAll() // wipe all state
await limiter.destroy() // stop cleanup timer (call on SIGTERM)
```

---

## Event Callbacks

All callbacks may be async; errors are swallowed and never crash the middleware.

```typescript
createRateLimiter({
  onBlocked: ({ key, blockDuration, blockCount }) => {
    console.log(`[BLOCKED] ${key} - offence #${blockCount}`)
  },
  onLimitReached: ({ key, count, limit }) => {
    /* last slot consumed */
  },
  onRequestAllowed: ({ key, remaining }) => {
    /* every allowed request */
  }
})
```

---

## TypeScript

Each adapter pre-binds `TReq` to the framework's native request type - you never write generics yourself.

```typescript
// Express - req is typed as Express Request
import { createRateLimiter } from 'awesome-rate-limiter/express'
createRateLimiter({
  keyGenerator: req => req.ip, // req: Express Request
  extractRequestData: req => ({ route: req.path }),
  onBlocked: ({ req }) => req.user?.id
})

// Fastify - swap the import, everything else stays the same
import { createRateLimiter } from 'awesome-rate-limiter/fastify'
createRateLimiter({
  keyGenerator: req => req.ip, // req: FastifyRequest
  onBlocked: ({ req }) => req.id
})
```

**Exported types:**

```typescript
import type {
  RateShieldContext,
  RateLimiterCore,
  RateLimitOptions,
  RateLimitInfo,
  RateLimitStats,
  RecordData,
  BlockedEvent,
  LimitReachedEvent,
  AllowedEvent
} from 'awesome-rate-limiter'

import type { RateLimiterInstance } from 'awesome-rate-limiter/express'
import type { FastifyRateLimiterInstance } from 'awesome-rate-limiter/fastify'
import type { KoaRateLimiterInstance } from 'awesome-rate-limiter/koa'
import type { HonoRateLimiterInstance } from 'awesome-rate-limiter/hono'

import type {
  RateLimitStore,
  StoreEntry,
  BlockRecord
} from 'awesome-rate-limiter/stores'
```

---

## Custom Framework Adapter

Any Node.js framework can be supported in ~10 lines. Implement `RateShieldContext<TReq>` and call `core.handle(ctx)`.

```typescript
import { createRateLimiter } from 'awesome-rate-limiter'
import type { RateShieldContext } from 'awesome-rate-limiter'

const core = createRateLimiter({ windowMs: 60_000, maxRequests: 100 })

async function myMiddleware(req: MyRequest, res: MyResponse, next: () => void) {
  const ctx: RateShieldContext<MyRequest> = {
    req,
    setHeader: (name, value) => res.setHeader(name, value),
    isResponseSent: () => res.writableEnded,
    block: (_code, body) => res.status(429).json(body),
    pass: () => next()
  }
  await core.handle(ctx)
}
```

| Method                   | Purpose                                              |
| ------------------------ | ---------------------------------------------------- |
| `req`                    | Passed to every callback as the typed request object |
| `setHeader(name, value)` | Write `X-RateLimit-*` and `Retry-After` headers      |
| `isResponseSent()`       | Guards against double-write                          |
| `block(status, body)`    | Send the 429 response                                |
| `pass()`                 | Continue to the next handler                         |

---

## Best Practices

1. **Shared store for multi-process deployments.** MemoryStore is per-process - Redis or PostgreSQL share state across cluster/PM2/Kubernetes.

2. **Trust proxy for real IPs.**
   - Express: `app.set('trust proxy', 1)`
   - Fastify: `Fastify({ trustProxy: true })`
   - Koa: `new Koa({ proxy: true })`

3. **Tighten limits on auth routes.** Use `createEndpointRateLimiter` with low `maxRequests` and a long `windowMs` for login, OTP, and password-reset routes.

4. **Call `limiter.destroy()` on shutdown.** Stops the cleanup `setInterval` - prevents Jest/Vitest from hanging after tests.

5. **Use `dryRun: true` before enforcing.** Observe traffic via `onRecord` and flip it off once you're confident in your limits.

6. **Whitelist health checks and internal services** to avoid false blocks.

7. **Match store TTL to `windowMs`.** Custom stores should set key expiry at least `windowMs * 2`.

---

## Contributors

<table>
  <tr>
    <td align="center">
      <a href="https://github.com/MozzammelRidoy">
        <img src="https://res.cloudinary.com/dsh57dvqf/image/upload/v1735985475/dx8q2l2x1ekijvqpgnud.jpg" width="120" height="120" style="border-radius:50%;" alt="Mozzammel Ridoy"/>
      </a>
      <br/>
      <b><a href="https://github.com/MozzammelRidoy">Mozzammel Ridoy</a></b>
      <br/>
      💻 Code • ⚙️ Maintenance
    </td>
  </tr>
</table>

---

## Reporting Issues

If you find a bug, have a feature idea, or want to discuss sponsorship, please reach out.

- **Bug / type error** - open a GitHub issue with a short description and reproduction steps
- **Feature request** - open a discussion or issue
- **Sponsorship / collaboration** - contact directly

📬 **Contact**

|              |                                                                          |
| ------------ | ------------------------------------------------------------------------ |
| **Email**    | [dev.mozzammelridoy@gmail.com](mailto:dev.mozzammelridoy@gmail.com)      |
| **WhatsApp** | [+8801889816198](https://wa.me/8801889816198)                            |
| **LinkedIn** | [linkedin.com/in/MozzammelRidoy](https://linkedin.com/in/MozzammelRidoy) |

---

MIT (c) [Mozzammel Ridoy](https://github.com/MozzammelRidoy)
