Ir al contenido
Español

Rate limit middleware

Esta página aún no está disponible en tu idioma.

rateLimit is a per-key request limiter. It returns a handler wrapper — call it with the options, then call the returned function with the handler you want to protect. Excess requests get 429 with a Retry-After header.

import { rateLimit } from 'actor-ts/http';
import { InMemoryCache } from 'actor-ts';
const limited = rateLimit({
cache: new InMemoryCache(),
windowMs: 60_000, // 1 minute window
max: 100, // 100 reqs per key per minute
key: (req) => req.headers['x-forwarded-for'] ?? 'unknown',
});
const routes = path('api', post(limited(apiHandler)));

Each distinct key (here: per X-Forwarded-For IP) gets a 100/minute budget. Excess requests get 429 with a Retry-After header.

interface RateLimitOptions {
cache: Cache;
windowMs: number;
max: number;
key: (req: HttpRequest) => string | Promise<string>;
keyPrefix?: string; // default 'rl:'
onLimit?: (ctx: RateLimitContext) => HttpResponse;
}
interface RateLimitContext {
key: string;
count: number;
max: number;
windowMs: number;
retryAfterSeconds: number;
}
FieldPurpose
cacheBacking store (in-memory or Redis).
windowMsFixed-window size.
maxMax requests per key per window.
key(req)Derive the rate-limit key — typically client IP, user ID, or API key. Required.
keyPrefixCache-key namespace. Default 'rl:' so multiple limiters in the same Redis don’t collide.
onLimit(ctx)Override the 429 response — full control over status / body / headers.
const limited = rateLimit({
cache,
windowMs: 60_000,
max: 100,
key: (req) => {
const userId = extractUserId(req);
return userId ? `user:${userId}` : `ip:${req.headers['x-forwarded-for'] ?? 'unknown'}`;
},
});

Per-user when authenticated; per-IP when not. Common pattern:

  • Auth’d routes — per-user limit.
  • Public routes — per-IP limit.

Combine with the framework’s Cache abstraction so the limiter works across pods with Redis backing.

Fixed window (default):
- Window starts when the first request in a key arrives.
- Increments a counter in the cache.
- When counter exceeds `max`, subsequent requests in the
window get 429.
- TTL on the counter ensures fresh windows start cleanly.

The implementation uses the cache’s incr with TTL — atomic across pods when backed by Redis.

The middleware sets:

X-RateLimit-Limit: 100
X-RateLimit-Remaining: 47
X-RateLimit-Reset: 1716297600 ← unix timestamp when window resets
Retry-After: 23 ← (on 429 only) seconds until reset

Clients use these to back off without retrying randomly.

import { RedisCache } from 'actor-ts';
rateLimit({
cache: new RedisCache({ url: 'redis://...' }),
windowMs: 60_000,
max: 100,
key: (req) => req.headers['x-forwarded-for'] ?? 'unknown',
});

With Redis-backed cache, all pods share the counter — a client hitting any pod accumulates against the same budget.

With InMemoryCache, each pod has its own counter — a client distributed across 4 pods gets effectively 4× the limit. Acceptable for permissive limits; problematic for strict ones.

Three good fits:

  1. Public APIs — prevent abuse / DoS.
  2. Tier-based limits — different key per tier (free / paid / enterprise).
  3. Per-action limits — login attempts, password resets, email-send rates.
// Different limits per handler:
const apiLimited = rateLimit({
cache, windowMs: 60_000, max: 1000,
key: (req) => req.headers['x-user-id'] ?? req.headers['x-forwarded-for'] ?? 'anon',
});
const loginLimited = rateLimit({
cache, windowMs: 60_000, max: 10,
key: (req) => `login:${req.headers['x-forwarded-for'] ?? 'unknown'}`,
});
const routes = concat(
path('login', post(loginLimited(loginHandler))),
path('orders', get(apiLimited(ordersHandler))),
);

Stricter limits on sensitive handlers; more permissive on general traffic. Compose by wrapping each handler with the matching limiter.