{"_id":"@backendkit-labs/rate-limiter","_rev":"2-88e6d72134ff09bf3673c1eb9b62f08c","name":"@backendkit-labs/rate-limiter","dist-tags":{"latest":"0.1.4"},"versions":{"0.1.0":{"name":"@backendkit-labs/rate-limiter","version":"0.1.0","keywords":["rate-limiter","rate-limiting","token-bucket","fixed-window","sliding-window","redis","nestjs","backendkit","resilience","backend"],"author":{"name":"BackendKit Labs","email":"hello@backendkitlabs.dev"},"license":"Apache-2.0","_id":"@backendkit-labs/rate-limiter@0.1.0","maintainers":[{"name":"backendkit.dev","email":"backendkit.dev@gmail.com"}],"homepage":"https://backendkitlabs.dev/docs/rate-limiter/","bugs":{"url":"https://github.com/BackendKit-labs/backendkit-monorepo/issues"},"dist":{"shasum":"597532d9a61edd1479b99f2ed99129db5f7a5535","tarball":"https://registry.npmjs.org/@backendkit-labs/rate-limiter/-/rate-limiter-0.1.0.tgz","fileCount":21,"integrity":"sha512-6Gx3HGiRG/yUNt+RkbIPhLbCbtOqaT1lBwCbqrxqVS/lOgSzWdVHTltfg8nkt6tPg5Du1gtVakKBPgHznxdFQA==","signatures":[{"sig":"MEUCIQDkpsbnIDcKc6Kf4hsoSmZcNKXKa2AYqkTYqauAsc1Z5wIgclUtJr+q3CVmrX17hn5nem6CmBw/RSRqEkiNKds2ZGg=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":326493},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","engines":{"node":">=18"},"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./nestjs":{"types":"./dist/nestjs/index.d.ts","import":"./dist/nestjs/index.js","require":"./dist/nestjs/index.cjs"}},"gitHead":"c058b04e2e08a9a06e2319163ddf729b638d8086","scripts":{"dev":"tsup --watch","lint":"eslint src/","test":"vitest run","build":"tsup","format":"prettier --write src/","typecheck":"tsc --noEmit","test:watch":"vitest","test:coverage":"vitest run --coverage","prepublishOnly":"npm run build && npm run test && npm run lint"},"_npmUser":{"name":"backendkit.dev","email":"backendkit.dev@gmail.com"},"repository":{"url":"git+https://github.com/BackendKit-labs/backendkit-monorepo.git","type":"git","directory":"packages/rate-limiter"},"_npmVersion":"11.8.0","description":"Modular rate limiter for Node.js — token bucket, fixed window, sliding window log & counter, with Redis atomic Lua scripts and optional NestJS integration","directories":{},"sideEffects":false,"_nodeVersion":"22.16.0","dependencies":{"@backendkit-labs/result":"*"},"publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"rxjs":"^7.8.0","tsup":"^8.0.0","eslint":"^9.0.0","vitest":"^2.0.0","ioredis":"^5.4.0","prettier":"^3.0.0","@eslint/js":"^9.39.4","typescript":"^5.5.0","@types/node":"^22.0.0","@nestjs/core":"^10.0.0","@nestjs/common":"^10.0.0","reflect-metadata":"^0.2.0","typescript-eslint":"^8.59.3"},"peerDependencies":{"ioredis":"^5.0.0","@nestjs/core":">=10.0.0","@nestjs/common":">=10.0.0","@backendkit-labs/observability":"*","@backendkit-labs/circuit-breaker":"*"},"peerDependenciesMeta":{"ioredis":{"optional":true},"@nestjs/core":{"optional":true},"@nestjs/common":{"optional":true},"@backendkit-labs/observability":{"optional":true},"@backendkit-labs/circuit-breaker":{"optional":true}},"_npmOperationalInternal":{"tmp":"tmp/rate-limiter_0.1.0_1779370874506_0.7556033986278088","host":"s3://npm-registry-packages-npm-production"}},"0.1.4":{"name":"@backendkit-labs/rate-limiter","version":"0.1.4","license":"Apache-2.0","author":{"name":"BackendKit Labs","email":"hello@backendkitlabs.dev"},"description":"Modular rate limiter for Node.js — token bucket, fixed window, sliding window log & counter, with Redis atomic Lua scripts and optional NestJS integration","type":"module","main":"./dist/index.cjs","module":"./dist/index.js","types":"./dist/index.d.ts","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./nestjs":{"types":"./dist/nestjs/index.d.ts","import":"./dist/nestjs/index.js","require":"./dist/nestjs/index.cjs"}},"scripts":{"build":"tsup","dev":"tsup --watch","test":"vitest run","test:watch":"vitest","test:coverage":"vitest run --coverage","typecheck":"tsc --noEmit","lint":"eslint src/","format":"prettier --write src/","prepublishOnly":"npm run build && npm run test && npm run lint"},"keywords":["rate-limiter","rate-limiting","token-bucket","fixed-window","sliding-window","redis","nestjs","backendkit","resilience","backend"],"homepage":"https://backendkitlabs.dev/docs/rate-limiter/","repository":{"type":"git","url":"git+https://github.com/BackendKit-labs/backendkit-monorepo.git","directory":"packages/rate-limiter"},"bugs":{"url":"https://github.com/BackendKit-labs/backendkit-monorepo/issues"},"publishConfig":{"access":"public","registry":"https://registry.npmjs.org"},"sideEffects":false,"engines":{"node":">=18"},"dependencies":{"@backendkit-labs/result":"*"},"peerDependencies":{"@backendkit-labs/circuit-breaker":"*","@backendkit-labs/observability":"*","@nestjs/common":">=10.0.0","@nestjs/core":">=10.0.0","ioredis":"^5.0.0"},"peerDependenciesMeta":{"@backendkit-labs/circuit-breaker":{"optional":true},"@backendkit-labs/observability":{"optional":true},"@nestjs/common":{"optional":true},"@nestjs/core":{"optional":true},"ioredis":{"optional":true}},"devDependencies":{"@eslint/js":"^9.39.4","@nestjs/common":"^10.0.0","@nestjs/core":"^10.0.0","@types/node":"^22.0.0","eslint":"^9.0.0","ioredis":"^5.4.0","prettier":"^3.0.0","reflect-metadata":"^0.2.0","rxjs":"^7.8.0","tsup":"^8.0.0","typescript":"^5.5.0","typescript-eslint":"^8.59.3","vitest":"^2.0.0"},"gitHead":"5e0fa1fa5f1ca51e2f01b6b68bcaa2a19e23cb3c","_id":"@backendkit-labs/rate-limiter@0.1.4","_nodeVersion":"22.16.0","_npmVersion":"11.8.0","dist":{"integrity":"sha512-QC5RjdDok1m5rx/zzBjMfViIboY5eJW5ag3/hSufLeKLeggtD3Bi30K1MaKyUmot1CTX2ADn9Ia+1j/0nUIgdw==","shasum":"80c270ec1438e090ce9d7237f9461e444ad5d988","tarball":"https://registry.npmjs.org/@backendkit-labs/rate-limiter/-/rate-limiter-0.1.4.tgz","fileCount":21,"unpackedSize":327049,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIG5T0GjQepUvaJZQUvJZ0U2BO9ibEd1Wap6MtJROmt6mAiEAqEr7AMddOHD4/bjQPMTKxe393Xmdv23VVAi0HXKIe3c="}]},"_npmUser":{"name":"backendkit.dev","email":"backendkit.dev@gmail.com"},"directories":{},"maintainers":[{"name":"backendkit.dev","email":"backendkit.dev@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/rate-limiter_0.1.4_1779643602842_0.7315974299301526"},"_hasShrinkwrap":false}},"time":{"created":"2026-05-21T13:41:14.314Z","modified":"2026-05-24T17:26:43.106Z","0.1.0":"2026-05-21T13:41:14.672Z","0.1.4":"2026-05-24T17:26:42.987Z"},"bugs":{"url":"https://github.com/BackendKit-labs/backendkit-monorepo/issues"},"author":{"name":"BackendKit Labs","email":"hello@backendkitlabs.dev"},"license":"Apache-2.0","homepage":"https://backendkitlabs.dev/docs/rate-limiter/","keywords":["rate-limiter","rate-limiting","token-bucket","fixed-window","sliding-window","redis","nestjs","backendkit","resilience","backend"],"repository":{"type":"git","url":"git+https://github.com/BackendKit-labs/backendkit-monorepo.git","directory":"packages/rate-limiter"},"description":"Modular rate limiter for Node.js — token bucket, fixed window, sliding window log & counter, with Redis atomic Lua scripts and optional NestJS integration","maintainers":[{"name":"backendkit.dev","email":"backendkit.dev@gmail.com"}],"readme":"# @backendkit-labs/rate-limiter\n\n[![npm version](https://img.shields.io/npm/v/@backendkit-labs/rate-limiter?style=flat-square&color=cb3837)](https://www.npmjs.com/package/@backendkit-labs/rate-limiter)\n[![CI](https://img.shields.io/github/actions/workflow/status/BackendKit-labs/backendkit-monorepo/ci.yml?style=flat-square&label=CI)](https://github.com/BackendKit-labs/backendkit-monorepo/actions/workflows/ci.yml)\n[![License](https://img.shields.io/npm/l/@backendkit-labs/rate-limiter?style=flat-square)](LICENSE)\n[![Node](https://img.shields.io/node/v/@backendkit-labs/rate-limiter?style=flat-square)](package.json)\n[![Docs](https://img.shields.io/badge/docs-backendkitlabs.dev-4f7eff?style=flat-square)](https://backendkitlabs.dev/docs/rate-limiter/)\n\n> Modular rate limiter for Node.js — token bucket, fixed window, sliding window log & counter, with Redis atomic Lua scripts and optional NestJS integration.\n\nFour battle-tested algorithms, one unified interface. Starts in-memory with no dependencies and scales to Redis without changing application code. Each algorithm returns a rich `RateLimitResult` so you can write correct `Retry-After` headers and expose standard `X-RateLimit-*` headers from a single result object.\n\nOptional NestJS integration included — module, guard, and method decorator.\n\n---\n\n## Minimal Example\n\nSelf-contained runnable example — Express server with all four algorithms and k6 load tests:\n\n```bash\ngit clone https://github.com/BackendKit-labs/backendkit-monorepo.git\ncd backendkit-monorepo/examples/rate-limiter-k6\nnpm install && npm start\n# then in another terminal:\nnpm run k6:smoke\n```\n\nShows token bucket, fixed window, sliding window, and multi-weight endpoints. k6 burst test reaches 50 VUs and verifies 100% fail-fast with zero 5xx. → [full source](https://github.com/BackendKit-labs/backendkit-monorepo/tree/master/examples/rate-limiter-k6)\n\n---\n\n## Table of Contents\n\n- [Installation](#installation)\n- [Quick Start](#quick-start)\n- [Algorithms](#algorithms)\n  - [Token Bucket](#token-bucket)\n  - [Fixed Window](#fixed-window)\n  - [Sliding Window Log](#sliding-window-log)\n  - [Sliding Window Counter](#sliding-window-counter)\n  - [Choosing an Algorithm](#choosing-an-algorithm)\n- [RateLimiter API](#ratelimiter-api)\n  - [consume()](#consume)\n  - [check()](#check)\n  - [reset() / resetAll()](#reset--resetall)\n  - [RateLimitResult](#ratelimitresult)\n- [Configuration Reference](#configuration-reference)\n- [Redis Store](#redis-store)\n  - [Basic Setup](#basic-setup)\n  - [Circuit Breaker Integration](#circuit-breaker-integration)\n- [Multi-weight Requests](#multi-weight-requests)\n- [Rate Limit Headers](#rate-limit-headers)\n- [NestJS Integration](#nestjs-integration)\n  - [Module Setup](#module-setup)\n  - [Guard — global or per-route](#guard--global-or-per-route)\n  - [Decorator — per method](#decorator--per-method)\n  - [Async Configuration](#async-configuration)\n- [TypeScript Configuration](#typescript-configuration)\n- [Architecture](#architecture)\n\n---\n\n## Installation\n\n```bash\nnpm install @backendkit-labs/rate-limiter\n```\n\nRedis support (optional peer dependency):\n\n```bash\nnpm install ioredis\n```\n\nNestJS peer dependencies (only needed for the `/nestjs` subpath):\n\n```bash\nnpm install @nestjs/common @nestjs/core rxjs\n```\n\nCircuit breaker for Redis resilience (optional):\n\n```bash\nnpm install @backendkit-labs/circuit-breaker\n```\n\n---\n\n## Quick Start\n\n```typescript\nimport { RateLimiterFactory, TokenBucketConfig } from '@backendkit-labs/rate-limiter';\n\nconst config: TokenBucketConfig = {\n  algorithm:        'token-bucket',\n  store:            'memory',\n  bucketSize:       10,\n  tokensPerSecond:  2,\n  keyPrefix:        'api:',\n};\n\nconst limiter = RateLimiterFactory.create(config);\n\n// In your HTTP handler:\nconst result = await limiter.consume(req.ip ?? 'unknown');\n\nif (!result.ok) {\n  // Store error — log and let the request through (fail open) or return 503\n  res.status(503).json({ error: 'rate_limiter_unavailable' });\n  return;\n}\n\nif (!result.value.allowed) {\n  res\n    .status(429)\n    .set('Retry-After', String(Math.ceil((result.value.resetAt - Date.now()) / 1000)))\n    .json({ error: 'too_many_requests', retryAfter: result.value.resetAt });\n  return;\n}\n\nres.json({ data: 'ok', remaining: result.value.remaining });\n```\n\n---\n\n## Algorithms\n\n### Token Bucket\n\nTokens accumulate at a fixed rate up to `bucketSize`. Each request consumes one (or more) tokens. Ideal for smooth bursty traffic — a client that has been idle earns tokens back and can briefly send a burst before being throttled.\n\n```typescript\nimport { RateLimiterFactory, TokenBucketConfig } from '@backendkit-labs/rate-limiter';\n\nconst config: TokenBucketConfig = {\n  algorithm:        'token-bucket',\n  store:            'memory',\n  bucketSize:       20,        // max burst\n  tokensPerSecond:  5,         // steady-state refill\n  initialTokens:    20,        // optional — defaults to bucketSize\n  keyPrefix:        'tb:',\n};\n\nconst limiter = RateLimiterFactory.create(config);\n```\n\n| Property | Type | Required | Description |\n|----------|------|----------|-------------|\n| `bucketSize` | `number` | yes | Maximum token capacity (max burst) |\n| `tokensPerSecond` | `number` | yes | Token refill rate |\n| `initialTokens` | `number` | no | Starting tokens — defaults to `bucketSize` |\n\n**When to use:** APIs that need to allow bursts (mobile clients, batch importers) while enforcing a long-term average rate.\n\n---\n\n### Fixed Window\n\nCounts requests inside fixed, non-overlapping time windows (e.g., 0–10s, 10–20s). Simple and efficient, but susceptible to a boundary burst — a client can send `2 × maxRequests` in a short window straddling a boundary.\n\n```typescript\nimport { RateLimiterFactory, FixedWindowConfig } from '@backendkit-labs/rate-limiter';\n\nconst config: FixedWindowConfig = {\n  algorithm:    'fixed-window',\n  store:        'memory',\n  windowMs:     60_000,   // 1 minute window\n  maxRequests:  100,\n  keyPrefix:    'fw:',\n};\n\nconst limiter = RateLimiterFactory.create(config);\n```\n\n| Property | Type | Required | Description |\n|----------|------|----------|-------------|\n| `windowMs` | `number` | yes | Window duration in milliseconds |\n| `maxRequests` | `number` | yes | Allowed requests per window |\n\n**When to use:** Simple per-minute or per-hour limits where the boundary burst is acceptable. Lowest memory footprint per key.\n\n---\n\n### Sliding Window Log\n\nStores a timestamp for every request. At consume time, entries older than `windowMs` are evicted and the count is checked. Exact enforcement with no boundary burst.\n\n```typescript\nimport { RateLimiterFactory, SlidingWindowLogConfig } from '@backendkit-labs/rate-limiter';\n\nconst config: SlidingWindowLogConfig = {\n  algorithm:    'sliding-window-log',\n  store:        'memory',\n  windowMs:     60_000,\n  maxRequests:  100,\n  keyPrefix:    'swl:',\n};\n\nconst limiter = RateLimiterFactory.create(config);\n```\n\n| Property | Type | Required | Description |\n|----------|------|----------|-------------|\n| `windowMs` | `number` | yes | Sliding window duration |\n| `maxRequests` | `number` | yes | Allowed requests in any `windowMs` span |\n\n**When to use:** Strict SLAs where the exact number of requests in any rolling window matters. Higher memory per key (O(maxRequests) timestamps).\n\n---\n\n### Sliding Window Counter\n\nHybrid approach — two fixed sub-windows with a weighted interpolation. Substantially more accurate than a plain fixed window, much lower memory than the log variant, because it only stores two counters per key.\n\n```typescript\nimport { RateLimiterFactory, SlidingWindowCounterConfig } from '@backendkit-labs/rate-limiter';\n\nconst config: SlidingWindowCounterConfig = {\n  algorithm:    'sliding-window-counter',\n  store:        'memory',\n  windowMs:     60_000,\n  maxRequests:  100,\n  keyPrefix:    'swc:',\n};\n\nconst limiter = RateLimiterFactory.create(config);\n```\n\n| Property | Type | Required | Description |\n|----------|------|----------|-------------|\n| `windowMs` | `number` | yes | Window duration |\n| `maxRequests` | `number` | yes | Allowed requests per window |\n\n**When to use:** High-traffic production APIs where memory matters and approximate sliding accuracy (±10%) is acceptable. Recommended default for most use cases.\n\n---\n\n### Choosing an Algorithm\n\n| Algorithm | Accuracy | Memory per key | Burst handling | Best for |\n|-----------|----------|---------------|----------------|----------|\n| Token Bucket | Exact | O(1) | Allows controlled bursts | APIs with bursty clients |\n| Fixed Window | Approximate | O(1) | Vulnerable to boundary burst | Simple quotas, low-traffic |\n| Sliding Window Log | Exact | O(maxRequests) | No boundary burst | Strict SLAs |\n| Sliding Window Counter | ~Exact | O(1) | Minimal boundary effect | High-traffic production default |\n\n---\n\n## RateLimiter API\n\n### `consume()`\n\nAttempts to consume one token (or `weight` tokens) for the given key. Returns a `Result<RateLimitResult, RateLimitError>`.\n\n```typescript\n// Consume one token\nconst result = await limiter.consume(clientKey);\n\n// Consume multiple tokens (multi-weight request)\nconst result = await limiter.consume(clientKey, 3);\n\nif (!result.ok) {\n  // Store failure — handle or let through\n  console.error(result.error.message);\n  return;\n}\n\nconst { allowed, remaining, resetAt, totalLimit } = result.value;\n```\n\nThe return type uses the `@backendkit-labs/result` monad:\n\n```typescript\ntype Result<T, E> =\n  | { ok: true;  value: T }\n  | { ok: false; error: E };\n```\n\n`!result.ok` means the **store** failed (Redis down, connection error) — not that the request was rate-limited. A rate-limited request returns `{ ok: true, value: { allowed: false, ... } }`.\n\n### `check()`\n\nReads current state without consuming a token. Useful for preflight checks or status endpoints.\n\n```typescript\nconst status = await limiter.check(clientKey);\n// Returns RateLimitResult (always, not wrapped in Result)\nconsole.log(status.remaining, status.resetAt);\n```\n\n### `reset() / resetAll()`\n\n```typescript\n// Reset a specific key\nawait limiter.reset(clientKey);\n\n// Reset all keys (clears the entire store)\nawait limiter.resetAll();\n```\n\n### `RateLimitResult`\n\n```typescript\ninterface RateLimitResult {\n  key:        string;   // The key that was consumed\n  allowed:    boolean;  // true = request allowed, false = rate limited (429)\n  remaining:  number;   // Tokens/requests remaining in the current window\n  resetAt:    number;   // Unix timestamp (ms) when the window resets\n  totalLimit: number;   // The configured limit (bucketSize or maxRequests)\n}\n```\n\n---\n\n## Configuration Reference\n\nAll algorithm configs extend the base `RateLimiterConfig`:\n\n```typescript\ninterface RateLimiterConfig {\n  algorithm:      AlgorithmType | IRateLimiterAlgorithm; // required\n  store?:         'memory' | 'redis' | IRateLimiterStore; // default: 'memory'\n  redisOptions?:  Record<string, unknown>;               // ioredis options when store='redis'\n  keyPrefix?:     string;                                // default: 'rl:'\n  circuitBreaker?: RateLimiterCircuitBreakerConfig;      // only affects redis store\n  logger?:        ILogger;                               // optional structured logger\n  metrics?:       IMetricsRecorder;                      // optional metrics recorder\n}\n```\n\n**Circuit breaker config** (only active when `store: 'redis'`):\n\n```typescript\ninterface RateLimiterCircuitBreakerConfig {\n  failureThreshold?: number;   // % Redis failures to open circuit (default: 50)\n  openTimeoutMs?:    number;   // ms to wait before probing Redis again (default: 30_000)\n  minimumCalls?:     number;   // min calls before evaluating threshold (default: 3)\n  slidingWindowSize?: number;  // window size (default: 5)\n  fallbackToMemory?: boolean;  // serve from in-process memory while open (default: true)\n  onStateChange?:    (from: string, to: string) => void;\n}\n```\n\n---\n\n## Redis Store\n\n### Basic Setup\n\nPass `store: 'redis'` and provide `redisOptions` — the library creates an `ioredis` client internally.\n\n```typescript\nimport { RateLimiterFactory, SlidingWindowCounterConfig } from '@backendkit-labs/rate-limiter';\n\nconst config: SlidingWindowCounterConfig = {\n  algorithm:    'sliding-window-counter',\n  store:        'redis',\n  redisOptions: {\n    host:     process.env['REDIS_HOST'] ?? '127.0.0.1',\n    port:     parseInt(process.env['REDIS_PORT'] ?? '6379', 10),\n    password: process.env['REDIS_PASSWORD'],\n    tls:      process.env['REDIS_TLS'] === 'true' ? {} : undefined,\n  },\n  windowMs:     60_000,\n  maxRequests:  100,\n  keyPrefix:    'api:rl:',\n};\n\nconst limiter = RateLimiterFactory.create(config);\n```\n\nThe Redis store runs all algorithm logic as **atomic Lua scripts** via `EVALSHA` (with `EVAL` fallback on `NOSCRIPT`). This means consume + check + update is a single round-trip with no race conditions — safe for multi-instance deployments.\n\nYou can also pass a pre-configured `ioredis` instance:\n\n```typescript\nimport Redis from 'ioredis';\nimport { RedisStore } from '@backendkit-labs/rate-limiter';\n\nconst redis = new Redis({ host: '127.0.0.1', port: 6379 });\nconst store = new RedisStore(redis);\n\nconst limiter = RateLimiterFactory.create({\n  algorithm:  'fixed-window',\n  store,\n  windowMs:   60_000,\n  maxRequests: 50,\n});\n```\n\n### Circuit Breaker Integration\n\nProtects your application when Redis is unavailable. When the circuit opens, the limiter transparently falls back to the in-process `MemoryStore`. Requires `@backendkit-labs/circuit-breaker`.\n\n```typescript\nconst config: SlidingWindowCounterConfig = {\n  algorithm:    'sliding-window-counter',\n  store:        'redis',\n  redisOptions: { host: process.env['REDIS_HOST'] },\n  windowMs:     60_000,\n  maxRequests:  100,\n  circuitBreaker: {\n    failureThreshold: 60,           // open after 60% Redis failures\n    openTimeoutMs:    30_000,       // probe Redis again after 30s\n    fallbackToMemory: true,         // serve in-memory while circuit is open\n    onStateChange: (from, to) => {\n      logger.warn(`Rate limiter Redis circuit: ${from} → ${to}`);\n    },\n  },\n};\n```\n\nWith `fallbackToMemory: true`, limits are enforced locally per instance while Redis recovers. Each instance has its own counter, so the effective limit is `maxRequests × instanceCount` during the outage — a reasonable trade-off for continued availability.\n\n---\n\n## Multi-weight Requests\n\nSome operations should cost more than one token — large uploads, expensive queries, batch endpoints.\n\n```typescript\n// This request costs 3 tokens\nconst result = await limiter.consume(req.ip ?? 'unknown', 3);\n\nif (result.ok && !result.value.allowed) {\n  res.status(429).json({ error: 'too_many_requests' });\n  return;\n}\n```\n\nToken Bucket is the most natural fit: a bucket of 20 tokens refilling at 5/s lets a client make up to 6 cheap (weight=1) requests or 1 expensive (weight=3) request in a given moment without conflating the two.\n\n---\n\n## Rate Limit Headers\n\nMap `RateLimitResult` to standard HTTP headers:\n\n```typescript\nfunction setRateLimitHeaders(res: Response, result: RateLimitResult): void {\n  const resetSec = Math.ceil(result.resetAt / 1000);\n  const retryAfter = Math.ceil(Math.max(result.resetAt - Date.now(), 0) / 1000);\n\n  res.set('X-RateLimit-Limit',     String(result.totalLimit));\n  res.set('X-RateLimit-Remaining', String(result.remaining));\n  res.set('X-RateLimit-Reset',     String(resetSec));\n\n  if (!result.allowed) {\n    res.set('Retry-After', String(retryAfter));\n  }\n}\n\n// Usage\nconst result = await limiter.consume(clientKey);\nif (result.ok) {\n  setRateLimitHeaders(res, result.value);\n  if (!result.value.allowed) {\n    res.status(429).json({ error: 'too_many_requests', retryAfter: result.value.resetAt });\n    return;\n  }\n}\n```\n\n---\n\n## NestJS Integration\n\nImport from the `/nestjs` subpath — NestJS code is tree-shaken from the core bundle.\n\n### Module Setup\n\n```typescript\nimport { RateLimiterModule } from '@backendkit-labs/rate-limiter/nestjs';\n\n@Module({\n  imports: [\n    RateLimiterModule.forRoot({\n      config: {\n        algorithm:       'sliding-window-counter',\n        store:           'memory',\n        windowMs:        60_000,\n        maxRequests:     100,\n      },\n      globalGuard: true, // registers RateLimiterGuard as APP_GUARD\n    }),\n  ],\n})\nexport class AppModule {}\n```\n\n`RateLimiterModule.forRoot()` provides:\n- `RateLimiterGuard` — registered as `APP_GUARD` when `globalGuard: true`\n- The configured `IRateLimiter` instance under the `RATE_LIMITER_INSTANCE` token\n\n### Guard — global or per-route\n\nWhen `globalGuard: true`, every route is protected by the default limiter. Use `@RateLimit()` to override or fine-tune individual routes.\n\n```typescript\nimport { Controller, Get } from '@nestjs/common';\nimport { RateLimit } from '@backendkit-labs/rate-limiter/nestjs';\n\n@Controller('api')\nexport class ApiController {\n\n  // Inherits the global limiter config\n  @Get('status')\n  status() {\n    return { status: 'ok' };\n  }\n\n  // Route-specific limit — overrides the global config for this endpoint\n  @Get('export')\n  @RateLimit({\n    algorithm:    'token-bucket',\n    store:        'memory',\n    bucketSize:   3,\n    tokensPerSecond: 0.1, // 1 export every 10 seconds\n    keyPrefix:    'export:',\n  })\n  export() {\n    return this.reportService.generate();\n  }\n\n  // Disable rate limiting for this route\n  @Get('health')\n  @RateLimit(null)\n  health() {\n    return { healthy: true };\n  }\n}\n```\n\nWhen the limit is exceeded, the guard returns:\n\n```json\nHTTP 429 Too Many Requests\nRetry-After: 42\nX-RateLimit-Limit: 100\nX-RateLimit-Remaining: 0\nX-RateLimit-Reset: 1716321600\n\n{ \"error\": \"too_many_requests\", \"retryAfter\": 1716321642000 }\n```\n\n### Decorator — per method\n\n`@RateLimit()` accepts a full `RateLimiterConfig` object (or any algorithm subtype). Each decorated route gets its own `RateLimiter` instance.\n\n```typescript\nimport { RateLimit, RateLimitOptions } from '@backendkit-labs/rate-limiter/nestjs';\n\n@Controller('payments')\nexport class PaymentsController {\n  @Post()\n  @RateLimit({\n    algorithm:       'token-bucket',\n    store:           'redis',\n    redisOptions:    { host: process.env['REDIS_HOST'] },\n    bucketSize:      5,\n    tokensPerSecond: 1,\n    keyPrefix:       'payments:',\n    circuitBreaker:  { fallbackToMemory: true },\n  })\n  charge(@Body() dto: ChargeDto) {\n    return this.paymentsService.charge(dto);\n  }\n}\n```\n\n### Async Configuration\n\nUse `forRootAsync` when config comes from `ConfigService` or other injectable providers:\n\n```typescript\nimport { RateLimiterModule } from '@backendkit-labs/rate-limiter/nestjs';\nimport { ConfigModule, ConfigService } from '@nestjs/config';\n\n@Module({\n  imports: [\n    ConfigModule.forRoot(),\n    RateLimiterModule.forRootAsync({\n      imports:    [ConfigModule],\n      inject:     [ConfigService],\n      useFactory: (config: ConfigService) => ({\n        algorithm:   'sliding-window-counter',\n        store:       'redis',\n        redisOptions: {\n          host:     config.get<string>('REDIS_HOST', '127.0.0.1'),\n          port:     config.get<number>('REDIS_PORT', 6379),\n          password: config.get<string>('REDIS_PASSWORD'),\n        },\n        windowMs:    config.get<number>('RATE_LIMIT_WINDOW_MS', 60_000),\n        maxRequests: config.get<number>('RATE_LIMIT_MAX_REQUESTS', 100),\n        circuitBreaker: { fallbackToMemory: true },\n      }),\n      globalGuard: true,\n    }),\n  ],\n})\nexport class AppModule {}\n```\n\n**Note — IP-based rate limiting behind a proxy:** When using the default key generator (based on `request.ip`), configure Express trust proxy in `main.ts` if your app runs behind a reverse proxy:\n\n```typescript\nconst app = await NestFactory.create(AppModule);\napp.set('trust proxy', 1); // trust one proxy hop (Nginx, ALB, Cloudflare, etc.)\n```\n\nWithout this, all clients behind the same proxy share a single rate limit key because `request.ip` returns the proxy's IP.\n\n---\n\n## TypeScript Configuration\n\n### Subpath exports (`/nestjs`)\n\nThis package uses the `exports` field in `package.json`. TypeScript's ability to resolve the `/nestjs` subpath depends on `moduleResolution`:\n\n**Modern resolution (recommended) — no extra config needed:**\n\n```json\n{\n  \"compilerOptions\": {\n    \"moduleResolution\": \"bundler\"\n  }\n}\n```\n\n`\"bundler\"`, `\"node16\"`, and `\"nodenext\"` all understand the `exports` field natively.\n\n**Legacy resolution (`\"node\"`) — add a `paths` alias:**\n\n```json\n{\n  \"compilerOptions\": {\n    \"moduleResolution\": \"node\",\n    \"paths\": {\n      \"@backendkit-labs/rate-limiter/nestjs\": [\n        \"./node_modules/@backendkit-labs/rate-limiter/dist/nestjs/index\"\n      ]\n    }\n  }\n}\n```\n\n### NestJS decorator support\n\n```json\n{\n  \"compilerOptions\": {\n    \"experimentalDecorators\": true,\n    \"emitDecoratorMetadata\": true\n  }\n}\n```\n\n---\n\n## Architecture\n\n```\n@backendkit-labs/rate-limiter          (core — zero framework dependencies)\n  RateLimiterFactory                   single-call factory, infers algorithm + store\n  RateLimiter                          consume() / check() / reset() / resetAll()\n  MemoryStore                          in-process store, zero dependencies\n  RedisStore                           atomic Lua scripts, ioredis single/cluster\n  TokenBucketAlgorithm                 smooth bursts, O(1) state\n  FixedWindowAlgorithm                 hard cap per window, O(1) state\n  SlidingWindowLogAlgorithm            exact sliding, O(maxRequests) state\n  SlidingWindowCounterAlgorithm        approximate sliding, O(1) state\n\n@backendkit-labs/rate-limiter/nestjs  (optional NestJS layer)\n  RateLimiterModule                    forRoot() / forRootAsync()\n  RateLimiterGuard                     APP_GUARD — returns 429 with Retry-After\n  @RateLimit()                         per-route config override or disable\n```\n\nThe core is a pure TypeScript library with a single runtime dependency (`@backendkit-labs/result`). `ioredis`, `@backendkit-labs/circuit-breaker`, and NestJS are all optional peer dependencies — none are loaded unless you explicitly use them.\n\n---\n\n## License\n\nApache-2.0 — [BackendKit Labs](https://github.com/BackendKit-labs)\n","readmeFilename":"README.md"}