nestjs-advanced-rate-limiter
v0.3.0
Published
Advanced rate limiter for NestJS. Token bucket (GCRA), sliding window, fixed window. In-memory or Redis. Drop-in replacement for @nestjs/throttler.
Maintainers
Readme
nestjs-advanced-rate-limiter
Advanced rate limiter for NestJS. Drop-in replacement for @nestjs/throttler with real algorithms: token bucket (GCRA), sliding window, and fixed window. In-memory or Redis.
Built on elysia-advanced-rate-limiter core -- the same algorithms Stripe and Cloudflare run in production.
Why not @nestjs/throttler?
| | @nestjs/throttler | nestjs-advanced-rate-limiter |
|---|---|---|
| Algorithms | TTL-based counter | Token bucket (GCRA), sliding window, fixed window |
| Burst control | None | GCRA allows controlled bursts while enforcing steady rate |
| Boundary accuracy | Resets at TTL expiry | Sliding window eliminates 2x burst at boundaries |
| Storage efficiency | Stores timestamps | 21-64 bytes per key depending on algorithm |
| Redis | Via separate adapter | Built-in RedisStore with circuit breaker (ResilientStore) |
| Per-route override | Multiple throttler configs | @Throttle() decorator with any algorithm |
| User tracking | Manual | Built-in JWT/session userTracker |
| IP resolution | Basic | Cloudflare, nginx, proxy-depth-aware |
Install
npm install nestjs-advanced-rate-limiterQuick Start
import { Module } from "@nestjs/common";
import { APP_GUARD } from "@nestjs/core";
import { ThrottlerModule, ThrottlerGuard } from "nestjs-advanced-rate-limiter";
@Module({
imports: [
ThrottlerModule.forRoot({
algorithm: { algorithm: "token-bucket", capacity: 100, refillRate: 10 },
}),
],
providers: [
{
provide: APP_GUARD,
useExisting: ThrottlerGuard,
},
],
})
export class AppModule {}Works out of the box. In-memory, token bucket, 100 capacity, 10 tokens/sec. Every route is protected.
Algorithms
Three algorithms. All O(1) time and space per request.
Token Bucket (GCRA) -- Default
Allows bursts up to capacity while enforcing a steady refillRate per second. Uses GCRA (Generic Cell Rate Algorithm) internally -- no refill loops, no locks, one number comparison.
ThrottlerModule.forRoot({
algorithm: { algorithm: "token-bucket", capacity: 100, refillRate: 10 },
})capacity = 5, refillRate = 1/sec
t=0ms Req #1 ALLOWED (5 tokens -> 4)
t=0ms Req #2 ALLOWED (4 tokens -> 3)
t=0ms Req #3 ALLOWED (3 tokens -> 2)
t=0ms Req #4 ALLOWED (2 tokens -> 1)
t=0ms Req #5 ALLOWED (1 tokens -> 0)
t=0ms Req #6 DENIED retryAfter=1s
t=1000 Req #7 ALLOWED (1 token refilled)Storage: 21 bytes per key
Fixed Window
Simplest option. Divides time into equal blocks, counts requests per block. Counter resets at each boundary.
ThrottlerModule.forRoot({
algorithm: { algorithm: "fixed-window", limit: 100, windowMs: 60_000 },
})Storage: 39 bytes per key
Note: Clients can send
limitrequests at the end of one window andlimitat the start of the next, getting 2x the limit in a short span. If this matters, use sliding window.
Sliding Window
Blends current and previous window counts to eliminate the boundary burst. Uses the two-counter approximation.
ThrottlerModule.forRoot({
algorithm: { algorithm: "sliding-window", limit: 100, windowMs: 60_000 },
})Storage: 64 bytes per key
Algorithm Comparison
Fixed Window Sliding Window Token Bucket (GCRA)
+----------------+------------------+--------------------+
Time | O(1) | O(1) | O(1) |
Space/key | 39 bytes | 64 bytes | 21 bytes |
Redis cmds | 1 INCR | INCR + GET | 1 EVAL (Lua) |
Burst | 2x at edges | Smoothed | Controlled |
Precision | Exact | Approximate | Exact |
Best for | Simplicity | Smooth limiting | APIs / billing |
+----------------+------------------+--------------------+Storage
In-Memory (default)
No dependencies. Good for single-process deployments.
import { MemoryStore } from "nestjs-advanced-rate-limiter";
ThrottlerModule.forRoot({
store: new MemoryStore({ maxKeys: 100_000, cleanupIntervalMs: 60_000 }),
})Redis
For multi-instance deployments. Works with ioredis or any compatible client.
import Redis from "ioredis";
import { RedisStore } from "nestjs-advanced-rate-limiter";
ThrottlerModule.forRoot({
store: new RedisStore(new Redis()),
})Resilient Store
Wraps any store with circuit breaker. If Redis goes down, your app keeps running.
import { ResilientStore, RedisStore } from "nestjs-advanced-rate-limiter";
ThrottlerModule.forRoot({
store: new ResilientStore(new RedisStore(redis), {
failMode: "open", // allow traffic when store is down (default)
threshold: 5, // open circuit after 5 consecutive failures
cooldownMs: 30_000, // retry after 30s
onError: (err) => console.error(err),
}),
})Decorators
@Throttle() -- Per-Route Override
Override the default algorithm for specific routes or controllers.
import { Controller, Post, Get } from "@nestjs/common";
import { Throttle } from "nestjs-advanced-rate-limiter";
@Controller("auth")
export class AuthController {
@Throttle({ algorithm: "fixed-window", limit: 5, windowMs: 60_000 })
@Post("login")
login() {
// strict: 5 attempts per minute
}
@Get("profile")
profile() {
// uses the global default
}
}@SkipThrottle() -- Skip Rate Limiting
Disable rate limiting for specific routes or entire controllers.
import { Controller, Get } from "@nestjs/common";
import { SkipThrottle } from "nestjs-advanced-rate-limiter";
@SkipThrottle()
@Controller("health")
export class HealthController {
@Get()
check() {
return "ok";
}
}User Tracking
Built-in JWT/session-based tracker. Identifies users from Authorization: Bearer <token> or session cookies. Falls back to IP.
import { userTracker } from "nestjs-advanced-rate-limiter";
ThrottlerModule.forRoot({
getTracker: userTracker(),
})
// Custom cookie name
ThrottlerModule.forRoot({
getTracker: userTracker({ cookieName: "session" }),
})
// Custom JWT parser
ThrottlerModule.forRoot({
getTracker: userTracker({
parseJwt: (token) => {
const payload = JSON.parse(atob(token.split(".")[1]));
return payload.sub;
},
}),
})
// Disable IP fallback
ThrottlerModule.forRoot({
getTracker: userTracker({ fallbackToIp: false }),
})IP Resolution
The default tracker (and userTracker fallback) checks headers in this order:
| Priority | Header | Set by |
|---|---|---|
| 1 | cf-connecting-ip | Cloudflare |
| 2 | x-real-ip | nginx / load balancer |
| 3 | x-forwarded-for (first IP) | Any proxy |
| 4 | req.ip / req.socket.remoteAddress | Express / Fastify |
Works with both Express and Fastify.
Async Configuration
Use forRootAsync when you need to inject dependencies (e.g., ConfigService).
import { ThrottlerModule } from "nestjs-advanced-rate-limiter";
import { ConfigModule, ConfigService } from "@nestjs/config";
@Module({
imports: [
ThrottlerModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
algorithm: {
algorithm: "token-bucket",
capacity: config.get("RATE_LIMIT_CAPACITY", 100),
refillRate: config.get("RATE_LIMIT_REFILL", 10),
},
}),
}),
],
})
export class AppModule {}All Options
ThrottlerModule.forRoot({
// Algorithm (default: token-bucket, 100 capacity, 10/sec)
algorithm: { algorithm: "token-bucket", capacity: 100, refillRate: 10 },
// Storage backend (default: in-memory)
store: new MemoryStore(),
// Key prefix for storage (default: "rl:")
prefix: "rl:",
// Custom tracker function (default: IP-based)
getTracker: (req) => req.ip,
// Programmatic skip (default: none)
skipIf: (req) => req.url.includes("/health"),
// Ignore specific user agents (default: [])
ignoreUserAgents: [/googlebot/i, /bingbot/i],
// Custom error message or response factory (default: "Too Many Requests")
errorMessage: "Rate limit exceeded",
// or
errorMessage: (result) => ({
error: "rate_limited",
retryAfter: Math.ceil(result.retryAfterMs / 1000),
}),
})Response Headers
Every response includes rate limit headers:
| Header | When | Example |
|---|---|---|
| X-RateLimit-Limit | Always | 100 |
| X-RateLimit-Remaining | Always | 95 |
| X-RateLimit-Reset | Always | 1712678460 (Unix timestamp) |
| Retry-After | 429 only | 5 (seconds) |
Migrating from @nestjs/throttler
- import { ThrottlerModule, ThrottlerGuard } from "@nestjs/throttler";
+ import { ThrottlerModule, ThrottlerGuard } from "nestjs-advanced-rate-limiter";
@Module({
imports: [
- ThrottlerModule.forRoot({ ttl: 60000, limit: 10 }),
+ ThrottlerModule.forRoot({
+ algorithm: { algorithm: "fixed-window", limit: 10, windowMs: 60_000 },
+ }),
],
providers: [{ provide: APP_GUARD, useExisting: ThrottlerGuard }],
})
export class AppModule {}The @SkipThrottle() decorator works the same way. Replace @Throttle() with the new config-based syntax:
- @Throttle({ default: { limit: 5, ttl: 60000 } })
+ @Throttle({ algorithm: "fixed-window", limit: 5, windowMs: 60_000 })License
MIT
