@fluojs/throttler
v2.0.2
Published
Decorator-based rate limiting for Fluo applications with in-memory and Redis store adapters.
Maintainers
Readme
@fluojs/throttler
Decorator-based rate limiting for fluo applications with in-memory and Redis store adapters.
Table of Contents
- Installation
- When to Use
- Quick Start
- Common Patterns
- NestJS Migration Boundaries
- Public API Overview
- Related Packages
- Example Sources
Installation
@fluojs/throttler declares engines.node >=24.0.0 <27 in its published package manifest as its package-owned support contract. Upgrade Node 20 and Node 22 hosts to Node.js >=24.0.0 <27; Node versions below 24 and Node 27+ are unsupported.
npm install @fluojs/throttlerWhen to Use
- To prevent brute-force attacks on sensitive endpoints (e.g., login, registration).
- To protect your API from being overwhelmed by too many requests from a single client.
- To implement usage quotas or tiered rate limits for different types of users.
- When you need a simple way to apply rate limits using decorators on controllers or methods.
Quick Start
This controller/module fragment belongs in an existing HTTP app. Retain the decorator/entrypoint configuration from Application Bootstrap Protocol. The module below registers the declared AuthController in controllers.
Register the ThrottlerModule, wire ThrottlerGuard with @UseGuards(...), and apply the Throttle decorator to controllers or methods that need route-specific limits.
import { Module } from '@fluojs/core';
import { ThrottlerGuard, ThrottlerModule, Throttle, SkipThrottle } from '@fluojs/throttler';
import { Controller, Post, UseGuards } from '@fluojs/http';
@Controller('/auth')
@UseGuards(ThrottlerGuard)
class AuthController {
@Post('/login')
@Throttle({ ttl: 60, limit: 5 }) // Override: 5 requests per minute
login() {
return { success: true };
}
@Post('/public-info')
@SkipThrottle() // Bypass throttling
getInfo() {
return { info: '...' };
}
}
@Module({
imports: [
ThrottlerModule.forRoot({
ttl: 60, // 60 seconds
limit: 10, // 10 requests
}),
],
controllers: [AuthController],
})
class AppModule {}Common Patterns
Redis Storage
For multi-instance deployments, use RedisThrottlerStore to share the rate limit state across all instances. Redis-backed windows are anchored to Redis server time, so distributed app nodes with clock skew still enforce one shared reset boundary.
RedisThrottlerStore accepts the package-local structural RedisThrottlerClient contract: a Redis command client with an eval(script, numberOfKeys, ...args) method. @fluojs/redis, ioredis, and compatible custom clients can be passed without making the root @fluojs/throttler import depend on a concrete ioredis constructor type.
import { ThrottlerModule, RedisThrottlerStore } from '@fluojs/throttler';
import { REDIS_CLIENT } from '@fluojs/redis';
// Inside a provider or module factory
const redisClient = await container.resolve(REDIS_CLIENT);
const redisStore = new RedisThrottlerStore(redisClient);
ThrottlerModule.forRoot({
ttl: 60,
limit: 100,
store: redisStore,
});You can also pass any object that implements the ThrottlerStore contract through the store option. ThrottlerModule.forRoot(...) validates that a custom store exposes a consume(...) function before request handling starts.
Custom Key Generation
By default, the throttler resolves client identity from the raw socket remoteAddress only. For a deployment behind a trusted reverse proxy that rewrites Forwarded, X-Forwarded-For, or X-Real-IP, configure trustProxy as the primary recommendation and restrict it to the proxy addresses your application actually observes. trustProxyHeaders: true is broad legacy compatibility only and is not recommended for new deployments. If no trusted socket or proxy identity is available, it throws instead of collapsing unrelated callers into a shared bucket. You can also customize this to use API keys, user IDs, or other identifiers.
Counters are scoped by route identity and client identity. The route portion includes module, controller, method, path, version, and the deterministic source/method position assigned by the HTTP route compiler. Distinct compiled handlers therefore remain isolated even when their display names and emitted source match, while identical artifact layouts produce the same identity across application instances for distributed bucket sharing. When a request is rejected, ThrottlerGuard returns 429 and sets Retry-After.
ThrottlerModule.forRoot({
ttl: 60,
limit: 100,
// Accept forwarding metadata only from this known reverse proxy.
trustProxy: ['192.0.2.10/32'],
});trustProxy is the explicit forwarding boundary for the default client key. It accepts false, a trusted-hop count, address/CIDR rules, or a predicate. Untrusted or malformed forwarding data cannot replace the direct transport identity. trustProxyHeaders: true preserves broad legacy full-header compatibility only; use trustProxy for new deployments.
ThrottlerModule.forRoot({
ttl: 60,
limit: 100,
keyGenerator: (context) => {
const apiKeyHeader = context.request.headers['x-api-key'];
const apiKey = Array.isArray(apiKeyHeader) ? apiKeyHeader[0] : apiKeyHeader;
if (!apiKey) {
throw new Error('Missing API key for throttler tracking.');
}
return `api-key:${apiKey}`;
},
});NestJS Migration Boundaries
When migrating from @nestjs/throttler, treat @fluojs/throttler as an explicit guard-stage package rather than a drop-in global limiter:
ThrottlerModule.forRoot(...)registers validated options and providers, but it does not automatically enforce throttling on every route. ActivateThrottlerGuardwith Fluo guard metadata such as@UseGuards(ThrottlerGuard)wherever enforcement is required.- The public policy shape is one module default plus class- or method-level
@Throttle({ ttl, limit })overrides. Named multi-window definitions such as burst plus sustained limits require explicit composition through HTTP middleware, a customThrottlerStore, or an application-owned guard wrapper. - NestJS
ttlvalues are milliseconds; fluottlvalues are seconds. Convertttl: 60_000tottl: 60rather than copying the value directly. @SkipThrottle()has no named orfalseform. Class- and method-level skips combine additively, so move a reactivated method outside a skipped controller or use an application-owned guard wrapper.- Resolve async secrets, configuration, and store construction before synchronous
ThrottlerModule.forRoot(...)registration; fluo does not provide NestJSforRootAsync(...)shapes. ThrottlerGuardandkeyGeneratorare HTTP-only. Apply separate transport-owned guards or middleware to WebSocket, GraphQL, RPC, and queue policies.- Persisted NestJS windows do not continue by default because bucket keys and storage call contracts differ. Use an application-owned compatibility store or a bounded cutover when continuity is required.
- Forwarded client IP headers are ignored by default. Prefer
trustProxyto declare a hop count, CIDR list, or predicate for a trusted proxy boundary.trustProxyHeaders: trueis broad legacy compatibility mode that trusts the complete forwarding chain, not only the direct peer, and is not recommended for new deployments. - The guaranteed limit-exceeded response contract is HTTP
429withRetry-After. Additional rate-limit headers or response bodies should be added at the application boundary, for example with an exception filter.
For migration examples and the complete compatibility map, see the NestJS → fluo Migration Map.
Public API Overview
Modules
ThrottlerModule.forRoot(options): Provides validated throttler options andThrottlerGuardto the module graph.ThrottlerModuleOptions: Public options shape accepted byThrottlerModule.forRoot(...).- Package-level registration is supported through
ThrottlerModule.forRoot(options). Internal provider-composition helpers and DI tokens are not part of the public contract.
ttl and limit must be positive finite integers. global defaults to true; set global: false when the throttler providers should stay scoped to the importing module. trustProxy is the recommended explicit boundary and uses TrustProxyPolicy from @fluojs/http; trustProxyHeaders is broad legacy compatibility only and is not recommended for new deployments. keyGenerator, when provided, must be a function. Module options are validated and captured by value when the guard is wired so later mutation of the caller's options object does not change live throttling policy. If no store option is supplied, each ThrottlerGuard instance owns its own in-memory store; pass a ThrottlerStore implementation such as RedisThrottlerStore when storage must be shared or externally managed.
Decorators
@Throttle({ ttl, limit }): Sets a specific rate limit for a class or method.@SkipThrottle(): Disables throttling for a class or method.ThrottlerHandlerOptions: Public{ ttl, limit }policy shape accepted by@Throttle(...). Both values must be positive finite integers; method-level policies override class-level policies, which override module defaults.- Existing root-barrel metadata helpers (
throttleRouteMetadataKey,getThrottleMetadata,getSkipThrottleMetadata,getClassThrottleMetadata, andgetClassSkipThrottleMetadata) remain exported for compatibility with advanced integrations that already inspect decorator metadata directly.
Guards
ThrottlerGuard: The guard responsible for enforcing rate limits.ThrottlerModule.forRoot()makes it injectable; route handlers still activate it through Fluo guard metadata such as@UseGuards(ThrottlerGuard).
Stores
createMemoryThrottlerStore(): Creates a simple in-memory store (default).RedisThrottlerStore: Store adapter for Redis.RedisThrottlerClient: Structural Redis command client contract accepted byRedisThrottlerStore.ThrottlerStore: Public contract for custom stores.ThrottlerConsumeInput: Public input shape passed toThrottlerStore.consume(key, input)so custom stores can share the guard's current time and TTL window.ThrottlerStoreEntry: Public result shape returned byThrottlerStore.consume(...);countis the post-consume request count for the active window andresetAtis the epoch-millisecond reset boundary used forRetry-Aftercalculation. Custom stores may return optionalretryAfterMswhen a backing store has a more authoritative clock than the application process; the guard uses it forRetry-Afterwhen the limit is exceeded.
Status and diagnostics
createThrottlerPlatformStatusSnapshot(...): Creates a platform status snapshot.createThrottlerPlatformDiagnosticIssues(...): Creates diagnostic issues for invalid throttler state.ThrottlerStatusAdapterInput: Public input shape for status and diagnostic helpers. It carries store kind, ownership mode, operation mode, backing-store readiness, dependency linkage, and readiness criticality hints collected during bootstrap.ThrottlerPlatformStatusSnapshot: Public output shape returned bycreateThrottlerPlatformStatusSnapshot(...), containing readiness, health, ownership, and details sections compatible with runtime platform snapshots.ThrottlerStoreKind: Store categories recognized by the status adapter:memory,redis, orcustom.ThrottlerStoreOwnershipMode: Ownership modes reported in status snapshots:frameworkfor guard-owned resources orexternalfor externally managed stores.ThrottlerOperationMode: Operation modes reported in status details:local-only,distributed,local-fallback, orcustom.
Method-level @Throttle(...) overrides class-level settings, class-level settings override module defaults, and @SkipThrottle() bypasses throttling at either class or method level.
Related Packages
@fluojs/http: Required for HTTP context and Exception handling.@fluojs/redis: Official Redis client integration forRedisThrottlerStore;ioredisand compatible structural clients are also supported.
Example Sources
packages/throttler/src/module.test.ts: Tests for module configuration, decorator overrides, and HTTP guard integration throughTest.createApp(...).packages/throttler/src/guard.ts: The core logic for request throttling and header management.packages/throttler/src/redis-store.test.ts: Redis store contract and server-time behavior.packages/throttler/src/status.test.ts: Status and diagnostic helper behavior.
Compatible service copies
ThrottlerGuard supports compatible-copy class injection; throttler options remain isolated.
