RecipesAuthentication

Per-User Rate Limiting

Key the rate limiter by authenticated user instead of IP address.

The default @nextrush/rate-limit key is the client IP — fine for anonymous traffic, but it under-protects you when many users share one IP (corporate NAT, mobile carrier) and over-throttles them together. If requests are already authenticated, key the limiter by user ID instead using keyGenerator.

Solution

src/index.ts
import { createApp, listen, errorHandler } from 'nextrush';
import { rateLimit } from '@nextrush/rate-limit';
import { requireAuth } from './middleware/require-auth'; // see the JWT recipe

const app = createApp();

app.use(errorHandler());

const perUserLimit = rateLimit({
  max: 100,
  window: '1m',
  // Falls back to the client IP for requests that somehow reach this
  // middleware unauthenticated — keyGenerator must always return a key.
  keyGenerator: (ctx) => {
    const user = ctx.state.user as { id: string } | undefined;
    return user ? `user:${user.id}` : `ip:${ctx.ip}`;
  },
});

app.get('/api/reports', requireAuth, perUserLimit, (ctx) => {
  ctx.json({ reports: [] });
});

listen(app, 8080);

keyGenerator is a real RateLimitOptions field (packages/middleware/rate-limit/src/types.ts) — it receives the full Context and returns the string key the limiter tracks state under, so each authenticated user gets an independent counter instead of sharing their IP's bucket. requireAuth must run before perUserLimit so ctx.state.user is populated by the time the key generator reads it. See Middleware for composition order.

Tiered limits by plan

If different user tiers (free/pro) need different limits rather than only different keys, use tieredRateLimit() with a tierResolver instead of a single rateLimit() call — see the rate-limit reference for TieredRateLimitOptions.

Try It

# Two different users, same IP — independent limits
curl http://localhost:8080/api/reports -H "Authorization: Bearer $TOKEN_USER_A"
curl http://localhost:8080/api/reports -H "Authorization: Bearer $TOKEN_USER_B"

Expected result: both succeed independently; each response carries RateLimit-Remaining scoped to its own user:<id> key, not a shared IP-based counter.

Was this helpful?

On this page