{"_id":"@adhavan_se_v/distributed-ratelimiter","name":"@adhavan_se_v/distributed-ratelimiter","dist-tags":{"latest":"1.0.1"},"versions":{"1.0.1":{"name":"@adhavan_se_v/distributed-ratelimiter","version":"1.0.1","description":"Distributed rate limiter Fastify plugin using Redis and Lua (Token Bucket & Sliding Window)","main":"index.js","type":"commonjs","keywords":["rate-limiter","redis","fastify","token-bucket","sliding-window","distributed"],"author":{"name":"Adhavan"},"license":"MIT","dependencies":{"fastify-plugin":"^5.1.0","ioredis":"^5.10.1"},"_id":"@adhavan_se_v/distributed-ratelimiter@1.0.1","gitHead":"f63625f5b7fdc78d469bb43e756fd9fefb7698fe","_nodeVersion":"25.2.1","_npmVersion":"11.1.0","dist":{"integrity":"sha512-t8Q0HkMxGZmGq9dd41DOkDeiAGMnSayKmAf+k0Q+Q5pAtsYK5BlGalKKWvPA1uvfmqGdeb2UMFPUQxjKLTw0MA==","shasum":"b286da0d16c8a4005b2d3a960fbea7dd1066103b","tarball":"https://registry.npmjs.org/@adhavan_se_v/distributed-ratelimiter/-/distributed-ratelimiter-1.0.1.tgz","fileCount":15,"unpackedSize":19680,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEYCIQCvDzGpK2S+2J45sAf/yM0Y+cyrIYa3933EZuGb1JxbGgIhAI10YQ89jy4v8uBQRKGMx5KvjNKGB00O4Q9gGTuLq3eB"}]},"_npmUser":{"name":"adhavan_se_v","email":"adhavankannan10@gmail.com"},"directories":{},"maintainers":[{"name":"adhavan_se_v","email":"adhavankannan10@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/distributed-ratelimiter_1.0.1_1774955029644_0.8933143810713764"},"_hasShrinkwrap":false}},"time":{"created":"2026-03-31T11:03:49.581Z","1.0.1":"2026-03-31T11:03:49.819Z","modified":"2026-03-31T11:03:49.965Z"},"maintainers":[{"name":"adhavan_se_v","email":"adhavankannan10@gmail.com"}],"description":"Distributed rate limiter Fastify plugin using Redis and Lua (Token Bucket & Sliding Window)","keywords":["rate-limiter","redis","fastify","token-bucket","sliding-window","distributed"],"author":{"name":"Adhavan"},"license":"MIT","readme":"# fastify-distributed-rate-limiter\r\n\r\n> A production-grade, distributed rate limiting plugin for Fastify — powered by Redis and atomic Lua scripts.\r\n\r\n[![npm version](https://img.shields.io/npm/v/fastify-distributed-rate-limiter.svg)](https://www.npmjs.com/package/fastify-distributed-rate-limiter)\r\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)\r\n[![Node.js](https://img.shields.io/badge/node-%3E%3D16.0.0-brightgreen)](https://nodejs.org)\r\n[![Redis](https://img.shields.io/badge/redis-%3E%3D6.0-red)](https://redis.io)\r\n\r\n---\r\n\r\n## Overview\r\n\r\n`fastify-distributed-rate-limiter` enforces request rate limits **across multiple server instances** using Redis as a centralized state store. It eliminates race conditions through atomic Lua scripts and supports multiple algorithms that can be selected dynamically per request.\r\n\r\nBuilt as a Fastify plugin — drop it in, configure it, and it works.\r\n\r\n---\r\n\r\n## Architecture\r\n\r\n<img width=\"1253\" height=\"835\" alt=\"image\" src=\"https://github.com/user-attachments/assets/cae236f7-0e65-49ce-825f-f28b1fdf9c8d\" />\r\n\r\n\r\nThe request flow:\r\n\r\n```\r\nClient → Load Balancer → Fastify Instance (preHandler Hook)\r\n                              ↓\r\n                       Strategy Layer\r\n                              ↓\r\n                    Redis + Lua Script (Atomic)\r\n                              ↓\r\n                     Allow / 429 Response\r\n```\r\n\r\nAll Fastify instances share a single Redis state store. Every rate-limiting decision is made atomically inside Redis via a Lua script — no locks, no race conditions.\r\n\r\n---\r\n\r\n## Features\r\n\r\n- **Multiple algorithms** — Token Bucket and Sliding Window Counter, selectable at runtime\r\n- **Atomic execution** — Lua scripts guarantee correctness under concurrency\r\n- **Distributed by design** — shared Redis state works across any number of instances\r\n- **Dynamic strategy selection** — choose algorithm per-route, per-user, or by any custom logic\r\n- **Flexible key design** — limit by IP, user ID, endpoint, or a combination\r\n- **Dynamic `Retry-After`** — computed inside Lua and returned via HTTP headers\r\n- **Observability** — built-in metrics endpoint tracking total, allowed, and blocked requests\r\n- **Config-driven** — no hardcoded limits; behavior defined via config function\r\n- **Redis flexibility** — supports local, cloud (TLS/password), or injected Redis clients\r\n\r\n---\r\n\r\n## Installation\r\n\r\n```bash\r\nnpm install fastify-distributed-rate-limiter\r\n```\r\n\r\n**Peer dependencies:**\r\n\r\n```bash\r\nnpm install fastify ioredis\r\n```\r\n\r\n---\r\n\r\n## Quick Start\r\n\r\n```javascript\r\nconst fastify = require(\"fastify\")({ logger: true, trustProxy: true });\r\nconst Redis = require(\"ioredis\");\r\n\r\n// ✅ Correct cloud Redis config\r\nconst redis = new Redis({\r\n  host: \"*******************************\",\r\n  port: ********,\r\n  username: \"*******\",\r\n  password: \"****************************\",\r\n});\r\n\r\n// ✅ Handle connection errors (important)\r\nredis.on(\"error\", (err) => {\r\n  console.error(\"Redis error:\", err.message);\r\n});\r\n\r\nfastify.register(require(\"ratelimiter\"), {\r\n  redisClient: redis,\r\n  limit: 10,\r\n  refillRate: 1,\r\n  algorithm: \"token_bucket\",\r\n});\r\n\r\nfastify.listen({ port: 4000 }, (err, address) => {\r\n  if (err) {\r\n    console.error(err);\r\n    process.exit(1);\r\n  }\r\n  console.log(`Server running at ${address}`);\r\n});\r\n```\r\n\r\n---\r\n\r\n## Configuration\r\n\r\n### Plugin Options\r\n\r\n| Option | Type | Required | Description |\r\n|---|---|---|---|\r\n| `redis` | `object \\| RedisClient` | Yes | Redis connection config or existing ioredis client |\r\n| `getConfig` | `function(req)` | Yes | Returns rate limit config for each request |\r\n| `keyGenerator` | `function(req)` | No | Custom Redis key generator |\r\n| `errorHandler` | `function(err, req, reply)` | No | Custom error handler |\r\n\r\n### `getConfig(req)` Return Object\r\n\r\n| Field | Type | Description |\r\n|---|---|---|\r\n| `algorithm` | `'token-bucket' \\| 'sliding-window'` | Rate limiting algorithm to use |\r\n| `limit` | `number` | Maximum requests allowed |\r\n| `windowMs` | `number` | Time window in milliseconds (sliding window) |\r\n| `capacity` | `number` | Max token capacity (token bucket) |\r\n| `refillRate` | `number` | Tokens per second refill rate (token bucket) |\r\n\r\n## Algorithms\r\n\r\n### Token Bucket\r\n\r\nBest for APIs that allow **burst traffic** — users can accumulate tokens and spend them in bursts, up to the bucket capacity.\r\n\r\n```javascript\r\ngetConfig: (req) => ({\r\n  algorithm: 'token_bucket',\r\n  capacity: 20,      // max burst size\r\n  refillRate: 5,     // tokens per second\r\n})\r\n```\r\n\r\n**How it works:**\r\n- Stores `tokens` and `last_refill` timestamp in Redis\r\n- On each request, refills tokens based on elapsed time, then attempts to consume one\r\n- If tokens are available: allow. If not: 429 with `Retry-After` header\r\n- All logic runs atomically inside a Lua script\r\n\r\n### Fixed Window Counter\r\n\r\nBest for **smooth, consistent rate limiting** — no burst allowance, requests are spread evenly.\r\n\r\n```javascript\r\ngetConfig: (req) => ({\r\n  algorithm: 'fixed_window',\r\n  limit: 100,\r\n  windowSize: 60,  // 60 seconds\r\n})\r\n```\r\n\r\n**How it works:**\r\n- Tracks counters for the current and previous time windows\r\n- Applies a weighted interpolation to estimate the request rate across the sliding boundary\r\n- More accurate and memory-efficient than storing individual request timestamps\r\n\r\n---\r\n\r\n## Redis Key Format\r\n\r\n```\r\nrate_limit:{algorithm}:{user_or_ip}:{route}\r\n```\r\n\r\n**Examples:**\r\n```\r\nrate_limit:sliding-window:192.168.1.1:/api/data\r\nrate_limit:token-bucket:user_abc123:/payment\r\n```\r\n\r\nCustom key generator:\r\n\r\n```javascript\r\nkeyGenerator: (req) => `rl:${req.user?.id ?? req.ip}:${req.routerPath}`\r\n```\r\n\r\n---\r\n\r\n## HTTP Headers\r\n\r\nOn every response, the plugin sets the following headers:\r\n\r\n| Header | Description |\r\n|---|---|\r\n| `X-RateLimit-Limit` | Maximum requests allowed |\r\n| `X-RateLimit-Remaining` | Requests remaining in the current window |\r\n| `X-RateLimit-Reset` | Unix timestamp when the window resets |\r\n| `Retry-After` | Seconds to wait before retrying (only on 429) |\r\n\r\n---\r\n\r\n## Metrics\r\n\r\nA built-in metrics endpoint is registered automatically:\r\n\r\n```\r\nGET /rate-limit/metrics\r\n```\r\n\r\n**Response:**\r\n```json\r\n{\r\n  \"total\": 10523,\r\n  \"allowed\": 10201,\r\n  \"blocked\": 322,\r\n  \"routes\": {\r\n    \"/api/data\": { \"total\": 8000, \"allowed\": 7900, \"blocked\": 100 },\r\n    \"/login\":    { \"total\": 2523, \"allowed\": 2301, \"blocked\": 222 }\r\n  }\r\n}\r\n```\r\n\r\n---\r\n\r\n## IP Handling\r\n\r\nThe plugin resolves the client IP in this order:\r\n\r\n1. `req.ip` (Fastify native, respects `trustProxy`)\r\n2. `x-forwarded-for` header (proxy environments)\r\n3. Falls back to direct remote address\r\n\r\n> **Important:** Enable `trustProxy: true` in your Fastify instance if your server sits behind a reverse proxy or load balancer.\r\n\r\n```javascript\r\nconst fastify = require('fastify')({ trustProxy: true });\r\n```\r\n\r\n---\r\n\r\n## Why Lua Scripts?\r\n\r\nEarly versions used multiple sequential Redis calls (`GET` → compute → `SET`), which caused **race conditions** under concurrent load: two requests could read the same counter, both pass, and both write back — effectively bypassing the limit.\r\n\r\nThe solution: move all logic into Redis using a **Lua script**. Redis executes Lua scripts atomically (single-threaded), so the read-compute-write cycle is always an uninterruptible unit. No locks needed.\r\n\r\n```lua\r\n-- Simplified token bucket Lua script\r\nlocal tokens = tonumber(redis.call('HGET', key, 'tokens'))\r\nlocal last   = tonumber(redis.call('HGET', key, 'last_refill'))\r\nlocal now    = tonumber(ARGV[1])\r\n\r\n-- Refill based on elapsed time\r\nlocal elapsed = (now - last) / 1000\r\ntokens = math.min(capacity, tokens + elapsed * refillRate)\r\n\r\nif tokens >= 1 then\r\n  tokens = tokens - 1\r\n  redis.call('HSET', key, 'tokens', tokens, 'last_refill', now)\r\n  return {1, tokens, 0}  -- allowed\r\nelse\r\n  local retry_after = math.ceil((1 - tokens) / refillRate)\r\n  return {0, 0, retry_after}  -- blocked\r\nend\r\n```\r\n\r\n---\r\n\r\n## Design Patterns\r\n\r\n| Pattern | Usage |\r\n|---|---|\r\n| **Strategy Pattern** | Pluggable algorithm selection at runtime |\r\n| **Middleware Pattern** | Fastify `preHandler` hook intercepts all requests |\r\n| **Plugin Pattern** | Encapsulated as a reusable Fastify plugin |\r\n| **Config-driven Design** | No hardcoded limits; behavior defined by caller |\r\n\r\n---\r\n\r\n## Use Cases\r\n\r\n- **API Gateways** — global request throttling across all services\r\n- **Authentication endpoints** — protect `/login` and `/register` from brute force\r\n- **Payment APIs** — controlled burst handling for sensitive transactions\r\n- **Public APIs** — enforce fair usage across anonymous and authenticated users\r\n\r\n---\r\n\r\n## Requirements\r\n\r\n- Node.js >= 16\r\n- Fastify >= 4\r\n- Redis >= 6\r\n- ioredis >= 5\r\n\r\n---\r\n","readmeFilename":"README.md","_rev":"1-0f677e480f20bd6566d3e430e4329ced"}