n4nAI

Rate limit LLM calls in NestJS with @nestjs/throttler

Learn how to implement nestjs throttler llm rate limiting in a NestJS app to protect LLM proxy routes from abuse, upstream limits, and cost spikes.

n4n Team2 min read529 words

Audio narration

Coming soon — every post will get a voice note here.

LLM endpoints are expensive and strictly metered by providers; a single runaway client can burn your budget or trip upstream 429s. Applying nestjs throttler llm rate limiting at the API gateway layer lets you cap request rates per user, per route, and per model before the call ever leaves your infrastructure.

Step 1: Install dependencies and scaffold the module

Start with a standard NestJS service. If you don’t have one, generate it with the CLI:

npm i -g @nestjs/cli
nest new llm-gateway
cd llm-gateway
npm i @nestjs/throttler @nestjs/config

@nestjs/throttler ships a guard, module, and decorator set that integrate with Nest’s dependency injection. For LLM workloads you’ll usually want Redis-backed storage so limits survive restarts and apply across instances. Add it now:

npm i @nestjs/throttler-storage-redis ioredis

The base nestjs throttler llm rate limiting setup is identical to any other route protection, but the thresholds and error handling differ because LLM calls are slow and costly.

Step 2: Register ThrottlerModule with sensible defaults

Wire the module in app.module.ts. Use forRootAsync to pull TTL and limit from env, and point storage at Redis:

import { ThrottlerModule } from '@nestjs/throttler';
import { ThrottlerStorageRedisService } from '@nestjs/throttler-storage-redis';
import Redis from 'ioredis';

@Module({
  imports: [
    ThrottlerModule.forRootAsync({
      useFactory: () => {
        const redis = new Redis(process.env.REDIS_URL);
        return {
          throttlers: [{ ttl: 60000, limit: 20 }],
          storage: new ThrottlerStorageRedisService(redis),
        };
      },
    }),
  ],
})
export class AppModule {}

This caps every route to 20 requests per minute globally. For LLM routes you’ll override per endpoint because a chat completion is not equivalent to a health check.

Step 3: Create an LLM-aware throttler guard

The default ThrottlerGuard keys on IP or user id. For LLM calls, key on the authenticated subject and the target model. Subclass the guard:

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

@Injectable()
export class LLMThrottlerGuard extends ThrottlerGuard {
  protected async getTracker(req: any): Promise<string> {
    const userId = req.user?.id ?? req.ip;
    const model = req.body?.model ?? 'default';
    return `${userId}:${model}`;
  }
}

Bind it globally or per controller. Global binding in main.ts:

app.useGlobalGuards(new LLMThrottlerGuard());

Now nestjs throttler llm rate limiting enforces isolation between models: a user hammering gpt-4o won’t exhaust their quota for gpt-4o-mini.

Step 4: Build the LLM proxy controller

Create a controller that forwards to an OpenAI-compatible endpoint. If you proxy through n4n.ai, its automatic fallback covers upstream provider degradation, but nestjs throttler llm rate limiting still caps your client traffic.

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

@Controller('llm')
export class LLMController {
  @Post('chat')
  @Throttle({ default: { ttl: 60000, limit: 5 } })
  async chat(@Body() body: { model: string; prompt: string }) {
    const res = await fetch('https://api.n4n.ai/v1/chat/completions', {
      method: 'POST',
      headers: {
        'content-type': 'application/json',
        authorization: `Bearer ${process.env.LLM_API_KEY}`,
      },
      body: JSON.stringify({
        model: body.model,
        messages: [{ role: 'user', content: body.prompt }],
      }),
    });
    return res.json();
  }
}

fetch is fine in Node 18+. The @Throttle decorator overrides the global 20/min with a stricter 5/min per model per user thanks to the guard in Step 3.

Step 5: Apply differentiated limits per route type

Embeddings and moderation calls are cheaper than generation. Reflect that with separate decorators:

@Post('embed')
@Throttle({ default: { ttl: 60000, limit: 50 } })
async embed(@Body() body: { input: string }) {
  // call embedding endpoint
}

@Post('chat')
@Throttle({ default: { ttl: 60000, limit: 5 } })
async chat(...) { ... }

This granular nestjs throttler llm rate limiting prevents a bulk embedding job from starving interactive chat users.

Step 6: Return proper 429 semantics

@nestjs/throttler throws ThrottlerException (HTTP 429) when limits exceed. Customize the response to include Retry-After and a JSON body:

import { ExceptionFilter, Catch, ArgumentsHost } from '@nestjs/common';
import { ThrottlerException } from '@nestjs/throttler';

@Catch(ThrottlerException)
export class ThrottlerFilter implements ExceptionFilter {
  catch(exception: ThrottlerException, host: ArgumentsHost) {
    const res = host.switchToHttp().getResponse();
    res.status(429).setHeader('Retry-After', '60');
    res.json({ error: 'rate_limited', message: exception.message });
  }
}

Register the filter in main.ts with app.useGlobalFilters(new ThrottlerFilter()). Clients then back off correctly instead of retrying immediately.

Step 7: Verify the limit with a loop

Write a quick bash loop to fire 10 requests at the chat route and observe the 429:

for i in {1..10}; do
  curl -s -o /dev/null -w "%{http_code}\n" -X POST localhost:3000/llm/chat \
    -H 'content-type: application/json' \
    -d '{"model":"gpt-4o-mini","prompt":"hi"}'
done

The first five return 200; the sixth returns 429. Check the header:

curl -i -X POST localhost:3000/llm/chat -d '{"model":"gpt-4o-mini","prompt":"hi"}' | grep retry-after

You should see Retry-After: 60. That confirms nestjs throttler llm rate limiting is active.

Production notes: tokens, not just requests

Throttler counts HTTP requests. A 10k-token prompt costs far more than a 10-token one. For tighter cost control, extend the guard to estimate tokens from req.body and weight the limit:

protected async getTracker(req: any): Promise<string> {
  const estTokens = Math.ceil((req.body?.prompt?.length ?? 0) / 4);
  req.throttleWeight = estTokens;
  return `${req.user?.id}:${req.body?.model}`;
}

Then override getLimit or use a custom storage that decrements by weight. Redis storage supports this if you track counters manually.

Run at least two instances behind a load balancer to confirm Redis storage shares state. Without it, each node enforces its own limit and the effective ceiling scales with node count.

That’s the full path: install, configure, subclass, proxy, decorate, filter, verify. Your LLM routes are now bounded by client, model, and route—no more surprise provider bills.

Tagsnestjsrate-limitingthrottlernodejs

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All nestjs llm api integration posts →