Skip to content
NestJS ns security 5 min read

Rate Limiting

Rate limiting caps how many requests a single client can make in a window of time, blunting brute-force attacks, credential stuffing, scraping, and accidental denial-of-service from runaway clients. NestJS ships an official package, @nestjs/throttler, that integrates as a guard so it slots neatly into the request lifecycle alongside your auth guards. This page covers configuring the module, wiring the global guard, tuning limits per route, skipping endpoints, and swapping in Redis so limits hold across a horizontally-scaled cluster.

Installing and configuring the module

Install the package, then register ThrottlerModule once at the root. Modern @nestjs/throttler (v5+) accepts an array of named throttlers, each with a ttl in milliseconds and a limit of requests allowed within that window.

npm install @nestjs/throttler
import { Module } from '@nestjs/common';
import { ThrottlerModule } from '@nestjs/throttler';

@Module({
  imports: [
    ThrottlerModule.forRoot([
      {
        name: 'short',
        ttl: 1000, // 1 second
        limit: 3, // 3 requests/second
      },
      {
        name: 'medium',
        ttl: 60_000, // 1 minute
        limit: 100,
      },
    ]),
  ],
})
export class AppModule {}

Defining multiple named throttlers lets a single endpoint be governed by several windows at once — a tight burst limit and a looser sustained limit — and a request is rejected if it exceeds any of them.

OptionTypeDescription
namestringIdentifier used to target the throttler from @Throttle.
ttlnumberWindow length in milliseconds.
limitnumberMax requests allowed per ttl window.
blockDurationnumberHow long (ms) to keep blocking after the limit is hit.
ignoreUserAgentsRegExp[]User agents (e.g. health checkers) to exempt.
storageThrottlerStorageBackend store; defaults to in-memory.

Enabling the global guard

The module only tracks counts — ThrottlerGuard enforces them. Register it as an APP_GUARD so every route is protected without per-controller boilerplate.

import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';
import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';

@Module({
  imports: [
    ThrottlerModule.forRoot([{ ttl: 60_000, limit: 100 }]),
  ],
  providers: [
    {
      provide: APP_GUARD,
      useClass: ThrottlerGuard,
    },
  ],
})
export class AppModule {}

When a client exceeds the limit, the guard throws a ThrottlerException and Nest responds with 429 Too Many Requests. It also emits standard rate-limit headers so well-behaved clients can back off.

Output:

HTTP/1.1 429 Too Many Requests
Retry-After: 1
X-RateLimit-Limit: 3
X-RateLimit-Remaining: 0
{ "statusCode": 429, "message": "ThrottlerException: Too Many Requests" }

Per-route limits with @Throttle

A global baseline is rarely right for every endpoint. Login and password-reset routes deserve far stricter limits than a public catalogue read. The @Throttle decorator overrides the module config for a single handler or controller, keyed by throttler name.

import { Controller, Post, Body } from '@nestjs/common';
import { Throttle, SkipThrottle } from '@nestjs/throttler';

@Controller('auth')
export class AuthController {
  // Tight limit: 5 attempts per minute on this route only.
  @Throttle({ medium: { ttl: 60_000, limit: 5 } })
  @Post('login')
  login(@Body() dto: LoginDto) {
    return this.authService.login(dto);
  }

  // No throttling on a cheap health check.
  @SkipThrottle()
  @Post('ping')
  ping() {
    return { ok: true };
  }
}

@SkipThrottle() disables all throttlers for the target, while @SkipThrottle({ short: true }) skips only a named one. Both decorators can be applied at the class level and selectively re-enabled per method.

Tip: Apply your strictest limits to authentication and write endpoints. These are the routes attackers hammer, and they are also the most expensive to serve.

Identifying clients behind a proxy

By default the guard keys limits on the request IP. Behind a load balancer or CDN every request appears to come from the proxy, so you must trust the forwarding header. Enable Express’s proxy trust and, if needed, override how the tracker derives a key by subclassing the guard.

import { Injectable } from '@nestjs/common';
import { ThrottlerGuard } from '@nestjs/throttler';

@Injectable()
export class IpThrottlerGuard extends ThrottlerGuard {
  protected async getTracker(req: Record<string, any>): Promise<string> {
    // Prefer the real client IP forwarded by the proxy.
    return req.ips.length ? req.ips[0] : req.ip;
  }
}
// main.ts
const app = await NestFactory.create(AppModule);
app.set('trust proxy', 1); // trust first proxy hop

You can also track by authenticated user id (req.user?.id) so a single account cannot evade limits by rotating IPs.

Distributed limits with Redis storage

The default in-memory store counts requests per process. The moment you run more than one instance, each node enforces its own quota and a client effectively multiplies its allowance by the instance count. A shared store fixes this — @nest-lab/throttler-storage-redis keeps counters in Redis so all instances see the same totals.

npm install @nest-lab/throttler-storage-redis ioredis
import { Module } from '@nestjs/common';
import { ThrottlerModule } from '@nestjs/throttler';
import { ThrottlerStorageRedisService } from '@nest-lab/throttler-storage-redis';
import Redis from 'ioredis';

@Module({
  imports: [
    ThrottlerModule.forRoot({
      throttlers: [{ ttl: 60_000, limit: 100 }],
      storage: new ThrottlerStorageRedisService(
        new Redis({ host: 'localhost', port: 6379 }),
      ),
    }),
  ],
})
export class AppModule {}

With Redis backing the counts, a rolling deploy, autoscaling event, or multi-region setup all enforce one coherent limit per client.

Warning: In-memory storage also resets on every restart, handing fresh quotas to attackers on each deploy. Use a shared store for any environment with more than one process.

Best Practices

  • Define both a short burst window and a longer sustained window so spikes and steady abuse are both contained.
  • Tighten limits sharply on login, register, and password-reset routes — these are the prime brute-force targets.
  • Always use a shared store (Redis) in production; per-process memory counts break under horizontal scaling.
  • Configure trust proxy and derive the tracker from the forwarded IP or user id so clients cannot bypass limits.
  • Exempt health checks and internal probes with @SkipThrottle() rather than whitelisting their traffic in code paths.
  • Surface Retry-After and rate-limit headers (enabled by default) so legitimate clients back off gracefully.
  • Log throttle hits as security events to spot attacks early, but never log full tokens or credentials.
Last updated June 14, 2026
Was this helpful?