Skip to main content
Rate limiting lives in the nestrs-throttle workspace member. It is the in-tree analogue of @nestjs/throttler: a ThrottlerGuard for declarative per-route limits, a throttler_middleware that applies the same skip → decorated → global precedence (via RouteRegistry lookup, because app-level layers cannot read HandlerKey), a ThrottlerService for manual checks from inside a handler, and pluggable storage (InMemoryThrottler for single-instance, RedisThrottler for distributed). ThrottleSpec is { limit, window_secs }. Parse decorator strings with ThrottleSpec::parse("5/minute") (second / minute / hour only). There is no ThrottlerOptions::builder(), no window_ms field, and no #[Throttle] PascalCase attribute.

Define a spec, register the module

The typical application path is NestApplication::use_throttler (feature throttler), which installs the 429 middleware and registers ThrottlerService for the guard:
ThrottlerOptions.global is the fallback for routes without #[throttle(...)]. None (the default) means only explicitly decorated routes are throttled.

Per-route limits

#[throttle(n, "second" | "minute" | "hour")] records metadata "throttle" => "n/per" and overrides the module global for that handler. #[skip_throttle] records "skip_throttle" => "true" and exempts the route entirely (wins over both the decorator and the global). One of each per handler — they do not stack into multiple windows. Rejected requests answer 429 with Retry-After and X-RateLimit-Remaining.

Manual checks

When a rate-limit decision depends on something the module can’t see (resource ownership, tenant tier), reach for the service directly. ThrottlerService::check takes (handler, key, spec) and scopes the backend key as {handler}:{key}:
The key string is whatever you want it to be — the service doesn’t care whether you derive it from an IP, a user id, or a hash of the request body.

Custom key generators

The default key is the client IP (resolved via the same trusted-proxy helpers as ClientIp). To rate-limit by API key instead, set ThrottlerOptions.key_generator:
ApiKeyHeaderKeyGenerator::default() reads x-api-key; ApiKeyHeaderKeyGenerator::new(header) overrides the name. Missing headers fall back to the IP so unkeyed traffic is still throttled. PrincipalKeyGenerator reads the PrincipalId request extension installed by authn / oauth2. ThrottleKeyGenerator is a trait over ThrottlerRequest<'_>handler, ip, and borrowed parts (headers, URI, extensions). There is no req.path field; use req.parts.uri.path().

Skip on demand

Internal services, health-check endpoints, and authenticated admins typically shouldn’t count against the limit. Implement ThrottleSkipper:
Install it as ThrottlerOptions { skipper: Some(Arc::new(SkipAdminRoutes)), .. }. NeverSkip is the default. Skipped requests neither consume counters nor generate blocks.

Distributed limits

Enable throttler-redis on the umbrella (maps to nestrs-throttle/cache-redis):
RedisThrottler uses an atomic INCR + EXPIRE pattern per spec key, so concurrent processes see the same counter. Connection failure at init falls back to in-memory (throttling is an optimization, not an availability gate). Sliding-window and token-bucket are not built in. Debug redacts user:pass@ from the Redis URL.

When to use what

  • #[throttle(n, "per")] on a route — the default. Limit and window are explicit and visible.
  • #[skip_throttle] — probes, admin, and internal paths. Raise limits on “important” routes; don’t skip them.
  • ThrottlerService::check from inside a handler — when the limit depends on handler-side state (per-tenant, per-resource).
  • ThrottleSkipper — bypass for a class of requests the decorator can’t see (internal IPs, a header).
  • Custom ThrottleKeyGenerator — when the IP-derived key is wrong for your traffic shape (NAT’d mobile clients, single egress proxy, per-tenant SaaS).

See also