@anysk/nestjs-redis-throttler
v1.0.0
Published
Redis-backed rate-limiting guard for NestJS with per-route overrides. No @nestjs/throttler dependency — fresh Nest peers, atomic Lua fixed windows, bring your own Redis client.
Maintainers
Readme
@anysk/nestjs-redis-throttler
Redis-backed rate limiting for NestJS — a standalone guard with per-route overrides and no @nestjs/throttler dependency.
Why it exists: rate-limiting packages that wrap or extend @nestjs/throttler inherit its peer range, so every Nest major leaves you waiting. This package depends only on @nestjs/common / @nestjs/core (peers: ^11 || ^12) and a tiny storage contract you can implement for any client. Counters live in Redis via one atomic Lua EVAL, so limits are shared across instances and survive restarts — which the default in-memory throttler storage never was.
- Fixed-window counting, atomic (INCR + PEXPIRE in one script; no TTL-less stray counters)
@Throttle()/@SkipThrottle()per-route overrides, same object shape as@nestjs/throttler- Multiple named windows (e.g. a minute burst limit plus an hourly cap)
- 429 responses carry
Retry-After - Only HTTP contexts are throttled by default — websocket/microservice/bot-framework contexts pass through
- Bring-your-own Redis client (structurally matches node-redis v4+; one-line ioredis adapter)
- Fail-open by default when Redis is briefly unavailable (configurable)
- Ships dual CJS + ESM, works under compilers that don't emit decorator metadata (esbuild/swc)
Install
npm i @anysk/nestjs-redis-throttler
# peers you already have in a Nest app: @nestjs/common @nestjs/core reflect-metadataQuickstart
// app.module.ts
import { APP_GUARD } from '@nestjs/core';
import { RedisThrottlerModule, RedisThrottlerGuard, RedisStore } from '@anysk/nestjs-redis-throttler';
@Module({
imports: [
RedisThrottlerModule.forRootAsync({
imports: [RedisModule],
inject: [RedisService],
useFactory: (redis: RedisService) => ({
windows: [
{ name: 'default', ttl: 60_000, limit: 60 },
{ name: 'hourly', ttl: 3_600_000, limit: 10_000 },
],
store: new RedisStore(redis.client), // any node-redis v4+ client
}),
}),
],
providers: [{ provide: APP_GUARD, useClass: RedisThrottlerGuard }],
})
export class AppModule {}Per-route overrides, by window name:
import { Throttle, SkipThrottle } from '@anysk/nestjs-redis-throttler';
@Controller('account')
export class AccountController {
@Throttle({ default: { ttl: 60_000, limit: 3 } }) // tighten the minute window here
@Post('sensitive')
sensitive() {}
@SkipThrottle()
@Get('health')
health() {}
}An override name that matches no configured window defines an extra window for that route only (both ttl and limit required):
@Throttle({ burst: { ttl: 1_000, limit: 2 } })Options
| Option | Default | Meaning |
| --- | --- | --- |
| windows | — | Named windows enforced on every route. ttl in ms. |
| store | — | RedisStore in production; InMemoryStore for tests/dev. |
| keyPrefix | "nrt" | Redis key prefix. Keys look like nrt:<window>:<Controller>:<handler>:<tracker>. |
| contextTypes | ["http"] | Execution context types to throttle; everything else passes through. |
| trustProxy | false | Key on the first X-Forwarded-For hop instead of req.ip. Prefer configuring your HTTP adapter's trust proxy; this is the escape hatch when you can't. |
| getTracker | req.ip | Custom client identity (e.g. user id). Return "" to skip the request. |
| skipIf | — | Predicate over the ExecutionContext to bypass throttling. |
| errorMessage | "Too Many Requests" | 429 body message, or a factory receiving the exceeded-limit detail. |
| onStoreError | "allow" | "allow" logs and lets requests through when the store throws; "block" rethrows. |
Requests with no resolvable identity are allowed, not pooled into one shared bucket — a misconfigured proxy shouldn't rate-limit all users collectively.
ioredis
new RedisStore({
eval: (script, { keys, arguments: args }) => ioredis.eval(script, keys.length, ...keys, ...args),
});Anything implementing ThrottlerStore (increment(key, ttlMs) → { totalHits, timeToExpireMs }) works as a store; the contract requires atomic counting where the first hit starts the TTL.
Migrating from @nestjs/throttler
- ThrottlerModule.forRoot([
- { name: 'default', ttl: 60_000, limit: 60 },
- { name: 'hourly', ttl: 3_600_000, limit: 10_000 },
- ]),
+ RedisThrottlerModule.forRootAsync({
+ imports: [RedisModule],
+ inject: [RedisService],
+ useFactory: (redis: RedisService) => ({
+ windows: [
+ { name: 'default', ttl: 60_000, limit: 60 },
+ { name: 'hourly', ttl: 3_600_000, limit: 10_000 },
+ ],
+ store: new RedisStore(redis.client),
+ }),
+ }),@Throttle({ default: { ttl, limit } })and@SkipThrottle()keep their shape — update the import.- A custom "http-only" guard subclass becomes configuration: non-HTTP contexts are skipped out of the box (
contextTypes). - Behavior parity: buckets are per window × route × client, 429 with
Retry-After. Difference: counters are in Redis, so limits hold across instances and restarts.
License
MIT
