Limitación de tasa de API: patrones y mejores prácticas
Cada punto final de API pública es un objetivo: los robots, los raspadores y los malos actores dañarán su servidor en el momento en que entre en funcionamiento. Sin limitación de tarifas, un solo cliente que se porte mal puede agotar las conexiones de su base de datos, aumentar su factura de la nube y cancelar el servicio para todos los usuarios legítimos. La limitación de tarifas no es opcional; es la primera línea de defensa para cualquier API de producción.
Esta guía analiza los cuatro algoritmos principales de limitación de velocidad, sus ventajas y desventajas y cómo implementarlos correctamente en NestJS con Redis. Saldrá con configuraciones de copiar y pegar para escenarios comunes y un modelo mental para elegir la estrategia correcta por punto final.
Conclusiones clave
- El depósito de fichas permite una explosión controlada, mientras que los contadores de ventana fijos son los más baratos de implementar.
- El registro de ventana deslizante es el más preciso pero consume mucha memoria; El mostrador de ventana corrediza es el mejor equilibrio.
- Redis es el único almacén de respaldo correcto cuando ejecuta múltiples réplicas de API
- NestJS
@nestjs/throttleradmite adaptadores de almacenamiento personalizados: intercambie en Redis con un cambio de configuración- Siempre devuelve los encabezados
Retry-After,X-RateLimit-Limit,X-RateLimit-RemainingyX-RateLimit-Reset- Diferenciar límites según la sensibilidad del punto final: autenticación (5/min) frente a API de lectura (1000/min)
- Utilice límites basados en IP para tráfico anónimo y límites basados en usuarios para solicitudes autenticadas
- Nunca abandones solicitudes en silencio; siempre devuelve
429 Too Many Requestscon un mensaje útil
Los cuatro algoritmos centrales
Mostrador de ventana fijo
El enfoque más simple: contar las solicitudes en un período de tiempo fijo y restablecerlas en el límite.
// Fixed window: 100 requests per minute
// Window resets at :00, :01, :02 ...
const windowKey = `ratelimit:${userId}:${Math.floor(Date.now() / 60000)}`;
const count = await redis.incr(windowKey);
await redis.expire(windowKey, 60);
if (count > 100) {
throw new TooManyRequestsException();
}
Debilidad: La explotación de límites. Un cliente puede enviar 100 solicitudes a las 12:00:59 y otras 100 a las 12:01:00; efectivamente, 200 solicitudes en dos segundos. Para la mayoría de las API, esto es aceptable. Para los puntos finales de autenticación, no lo es.
Registro de ventana deslizante
Almacene cada marca de tiempo de solicitud. En cada solicitud, cuente las marcas de tiempo en la última ventana.
const now = Date.now();
const windowStart = now - 60_000; // 60 seconds ago
// Remove old entries, add current
await redis.zremrangebyscore(key, 0, windowStart);
await redis.zadd(key, now, now.toString());
const count = await redis.zcard(key);
await redis.expire(key, 60);
if (count > 100) {
throw new TooManyRequestsException();
}
Compensación: Perfectamente preciso pero almacena O(n) entradas por usuario donde n es el recuento de solicitudes. A 1000 RPS para 10 000 usuarios, su memoria de Redis aumenta rápidamente. Úselo para puntos finales de bajo volumen y alta seguridad, como el restablecimiento de contraseña.
Mostrador de ventana corrediza
Ventana corrediza aproximada usando dos ventanas fijas, sin explosión de memoria.
const now = Date.now();
const currentWindow = Math.floor(now / 60000);
const previousWindow = currentWindow - 1;
const windowProgress = (now % 60000) / 60000; // 0.0 to 1.0
const [current, previous] = await redis.mget(
`rl:${userId}:${currentWindow}`,
`rl:${userId}:${previousWindow}`
);
const estimated =
(parseInt(previous ?? '0') * (1 - windowProgress)) +
parseInt(current ?? '0');
if (estimated >= 100) {
throw new TooManyRequestsException();
}
await redis.incr(`rl:${userId}:${currentWindow}`);
await redis.expire(`rl:${userId}:${currentWindow}`, 120);
Este es el algoritmo que utiliza Cloudflare. Suaviza el pico de límites con una sobrecarga mínima: dos claves de Redis por usuario y por ventana.
Cubo de fichas
El estándar de oro para permitir ráfagas manteniendo una tasa a largo plazo. Cada usuario tiene un cubo que se llena a un ritmo constante. Las solicitudes consumen tokens.
async function consumeToken(
redis: Redis,
userId: string,
ratePerSec: number,
capacity: number
): Promise<boolean> {
const now = Date.now() / 1000;
const key = `bucket:${userId}`;
const values = await redis.hmget(key, 'tokens', 'lastRefill');
const currentTokens = parseFloat(values[0] ?? String(capacity));
const lastRefillTime = parseFloat(values[1] ?? String(now));
// Refill tokens based on elapsed time
const elapsed = now - lastRefillTime;
const refilled = Math.min(capacity, currentTokens + elapsed * ratePerSec);
if (refilled < 1) {
return false; // No tokens available
}
await redis.hset(key, 'tokens', String(refilled - 1), 'lastRefill', String(now));
await redis.expire(key, Math.ceil(capacity / ratePerSec) + 60);
return true;
}
El depósito de tokens es ideal para API que necesitan permitir ráfagas cortas (cargar 10 archivos a la vez) y al mismo tiempo evitar el abuso sostenido.
Configuración del acelerador NestJS
@nestjs/throttler v5 se envía con un adaptador de almacenamiento Redis. Aquí hay una configuración lista para producción:
pnpm add @nestjs/throttler @nestjs/throttler-storage-redis ioredis
// app.module.ts
import { ThrottlerModule, ThrottlerGuard } from '@nestjs/throttler';
import { ThrottlerStorageRedisService } from '@nestjs/throttler-storage-redis';
import { APP_GUARD } from '@nestjs/core';
@Module({
imports: [
ThrottlerModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
throttlers: [
{ name: 'short', ttl: 1000, limit: 5 }, // 5 req/sec burst
{ name: 'medium', ttl: 60000, limit: 300 }, // 300 req/min
{ name: 'long', ttl: 3600000, limit: 5000 }, // 5000 req/hr
],
storage: new ThrottlerStorageRedisService({
host: config.get('REDIS_HOST'),
port: config.get('REDIS_PORT'),
}),
}),
}),
],
providers: [
{
provide: APP_GUARD,
useClass: ThrottlerGuard,
},
],
})
export class AppModule {}
Anulación de límites por controlador o ruta:
@Controller('auth')
export class AuthController {
// Authentication: very strict — 5 attempts per minute
@Post('login')
@Throttle({ medium: { ttl: 60000, limit: 5 } })
async login(@Body() dto: LoginDto) { /* ... */ }
// Refresh: moderate — 30 per minute
@Post('refresh')
@Throttle({ medium: { ttl: 60000, limit: 30 } })
async refresh(@Body() dto: RefreshDto) { /* ... */ }
// Skip throttling on the exchange endpoint (protected by one-time code TTL)
@Post('exchange')
@SkipThrottle()
async exchange(@Body() dto: ExchangeDto) { /* ... */ }
}
Generadores de claves personalizados
De forma predeterminada, el acelerador NestJS utiliza la IP del cliente. En producción detrás de Nginx/Cloudflare, necesita X-Real-IP o CF-Connecting-IP.
// throttler-behind-proxy.guard.ts
import { ThrottlerGuard } from '@nestjs/throttler';
import { Injectable, ExecutionContext } from '@nestjs/common';
@Injectable()
export class ThrottlerBehindProxyGuard extends ThrottlerGuard {
protected async getTracker(req: Record<string, any>): Promise<string> {
// Authenticated user — use userId for accurate per-user limits
if (req.user?.sub) {
return `user:${req.user.sub}`;
}
// Anonymous — use real IP from Cloudflare header
return (
req.headers['cf-connecting-ip'] ||
req.headers['x-real-ip'] ||
(req.headers['x-forwarded-for'] as string)?.split(',')[0] ||
req.ip
);
}
protected async throwThrottlingException(
context: ExecutionContext,
throttlerLimitDetail: ThrottlerLimitDetail
): Promise<void> {
const response = context.switchToHttp().getResponse();
response.header(
'Retry-After',
Math.ceil(throttlerLimitDetail.ttl / 1000)
);
await super.throwThrottlingException(context, throttlerLimitDetail);
}
}
Encabezados de respuesta
RFC 6585 y los encabezados borrador de RateLimit indican a los clientes exactamente cuándo volver a intentarlo:
// rate-limit.interceptor.ts
import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nestjs/common';
import { Observable, tap } from 'rxjs';
@Injectable()
export class RateLimitHeadersInterceptor implements NestInterceptor {
intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
return next.handle().pipe(
tap(() => {
const res = context.switchToHttp().getResponse();
const req = context.switchToHttp().getRequest();
// Values injected by ThrottlerGuard after evaluation
if (req.rateLimit) {
res.set({
'X-RateLimit-Limit': req.rateLimit.limit,
'X-RateLimit-Remaining': Math.max(
0,
req.rateLimit.limit - req.rateLimit.current
),
'X-RateLimit-Reset': new Date(
Date.now() + req.rateLimit.ttl
).toISOString(),
'RateLimit-Policy': `${req.rateLimit.limit};w=${Math.ceil(
req.rateLimit.ttl / 1000
)}`,
});
}
})
);
}
}
Estrategias específicas para endpoints
Diferentes puntos finales garantizan límites diferentes. Aquí hay una tabla de referencia para patrones comunes:
| Tipo de punto final | Algoritmo | Límite | Ventana |
|---|---|---|---|
| Iniciar sesión/restablecer contraseña | Registro de ventana corredera | 5 | 15 minutos |
| Verificación OTP / 2FA | Ventana fija | 3 | 10 minutos |
| API de lectura pública | Cubo de fichas | 1000 ráfagas, 100/s de relleno | — |
| API de mutación (autenticada) | Mostrador de ventana corredera | 300 | 1 minuto |
| Ingestión de webhook | Ventana fija | 10.000 | 1 minuto |
| Carga de archivos | Cubo de fichas | 10 ráfagas, 1/s de llenado | — |
| Puntos finales de IA/LLM | Ventana fija | 20 | 1 minuto |
| Búsqueda (anónima) | Ventana fija | 30 | 1 minuto |
Scripts atómicos de Lua para seguridad distribuida
Cuando tiene varias réplicas de API, las condiciones de carrera en secuencias de verificación incremental pueden permitir ráfagas por encima del límite. Utilice un script Lua cargado a través de redis.defineCommand para hacer que la verificación e incremento sea atómica:
// rate-limit.service.ts
import { Injectable } from '@nestjs/common';
import Redis from 'ioredis';
@Injectable()
export class RateLimitService {
constructor(private readonly redis: Redis) {
// Define atomic increment+check as a custom Redis command
this.redis.defineCommand('rateLimitCheck', {
numberOfKeys: 1,
lua: `
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local ttlMs = tonumber(ARGV[2])
local count = redis.call('INCR', key)
if count == 1 then
redis.call('PEXPIRE', key, ttlMs)
end
if count > limit then
return {0, redis.call('PTTL', key)}
end
return {1, -1}
`,
});
}
async isAllowed(
key: string,
limit: number,
windowMs: number
): Promise<{ allowed: boolean; retryAfterMs: number }> {
const result = await (this.redis as any).rateLimitCheck(
key, limit, windowMs
) as [number, number];
return {
allowed: result[0] === 1,
retryAfterMs: result[1] > 0 ? result[1] : 0,
};
}
}
Degradación elegante y estrategias de derivación
La limitación de tarifas no debe bloquear los controles de estado internos, los agentes de monitoreo o los socios confiables.
// trusted-bypass.guard.ts
@Injectable()
export class RateLimitWithBypassGuard extends ThrottlerBehindProxyGuard {
private readonly trustedTokens = new Set([
process.env.MONITORING_TOKEN,
process.env.PARTNER_API_TOKEN,
].filter(Boolean));
async canActivate(context: ExecutionContext): Promise<boolean> {
const req = context.switchToHttp().getRequest();
// Internal health checks bypass all rate limits
if (req.path === '/health' || req.path === '/ready') {
return true;
}
// Trusted API tokens bypass
const token = req.headers['x-bypass-token'];
if (token && this.trustedTokens.has(token)) {
return true;
}
return super.canActivate(context);
}
}
Para una limitación de velocidad progresiva (advertir antes del bloqueo), devuelva 429 con un encabezado Retry-After solo después de cruzar el 80% del límite:
// In your custom guard, after counting requests:
if (count > limit * 0.9 && count <= limit) {
response.set('X-RateLimit-Warning', 'approaching limit');
}
if (count > limit) {
response.set('Retry-After', retryAfterSeconds.toString());
throw new HttpException('Rate limit exceeded', 429);
}
Limitación de la tasa de prueba
// rate-limit.spec.ts
describe('Rate Limiting', () => {
it('should block after limit exceeded', async () => {
const app = moduleRef.createNestApplication();
await app.init();
// Hit the endpoint 5 times (limit for login)
for (let i = 0; i < 5; i++) {
await request(app.getHttpServer())
.post('/auth/login')
.send({ email: '[email protected]', password: 'wrongpassword' })
.expect((res) => expect(res.status).toBeLessThan(429));
}
// 6th request should be blocked
const response = await request(app.getHttpServer())
.post('/auth/login')
.send({ email: '[email protected]', password: 'wrongpassword' });
expect(response.status).toBe(429);
expect(response.headers['retry-after']).toBeDefined();
expect(response.body.message).toContain('rate limit');
});
});
Monitoreo y alertas
Los eventos de límite de tasa son señales valiosas. Regístrelos en su plataforma de observabilidad:
@Injectable()
export class RateLimitMetricsService {
async recordRateLimitHit(userId: string, endpoint: string, ip: string) {
await this.metricsService.increment('rate_limit.hits', {
endpoint,
user_type: userId ? 'authenticated' : 'anonymous',
});
// Alert on sustained attacks (>100 hits in 1 min from same IP)
const alertKey = `rl_alert:${ip}`;
const recentHits = await this.redis.incr(alertKey);
if (recentHits === 1) {
await this.redis.expire(alertKey, 60);
}
if (recentHits === 100) {
await this.alertService.send({
severity: 'high',
message: `Rate limit attack detected from IP ${ip}`,
endpoint,
});
}
}
}
Crear paneles de seguimiento:
- Tasa de aciertos límite por punto final (p95, p99)
- Principales IP/usuarios que alcanzan los límites
- Porcentaje de solicitudes bloqueadas frente a atendidas
- Duraciones de reintento posterior para detectar clientes mal configurados
Preguntas frecuentes
¿Debo calificar el límite por IP o por ID de usuario?
Utilice ambos. Para puntos finales no autenticados, la IP es el único identificador disponible. Para puntos finales autenticados, prefiera siempre la identificación de usuario: es más precisa y evita que una IP compartida (como una NAT corporativa) bloquee a todos los empleados. Implemente una verificación de dos niveles: límite de IP en el nivel de Nginx y límite de ID de usuario en el nivel de la aplicación.
¿Cuál es el código de estado HTTP correcto para la limitación de velocidad?
Siempre 429 Too Many Requests según RFC 6585. Nunca use 503 Service Unavailable (implica falla de infraestructura) o 403 Forbidden (implica falla de autorización). Incluya Retry-After como encabezado en segundos para que los clientes sepan cuándo volver a intentarlo.
¿Cómo manejo la limitación de velocidad detrás de Cloudflare o un balanceador de carga?
Configure su proxy para establecer X-Real-IP o CF-Connecting-IP y confíe solo en el rango de IP de su proxy. En Nginx: set_real_ip_from 103.21.244.0/22; real_ip_header CF-Connecting-IP;. En NestJS, configure app.set('trust proxy', 1) y lea req.ip que NestJS resuelve desde el encabezado del proxy confiable.
¿Qué estructura de datos de Redis es mejor para limitar la velocidad?
Para contadores de ventana fija/corrediza, use INCR + EXPIRE en una clave de cadena: O(1) por solicitud. Para el registro de ventana deslizante, utilice un conjunto ordenado (ZADD, ZREMRANGEBYSCORE, ZCARD) — O(log n). Para el depósito de tokens, utilice un hash (HSET, HGET) — O(1). Los scripts Lua hacen que todas las operaciones sean atómicas en los tres patrones.
¿Cómo debo manejar los límites de tarifas para webhooks de proveedores confiables?
Stripe, GitHub y proveedores similares envían webhooks desde rangos de IP conocidos. Mantenga una lista de permitidos de sus rangos CIDR y omita la limitación de velocidad para esas IP en su punto final de ingestión de webhook. Primero verifique la firma del webhook: la verificación de la firma es su capa de seguridad real, no la limitación de velocidad.
¿Puedo implementar una limitación de velocidad en el nivel de Nginx?
Sí, y debería hacerlo para obtener protección básica contra DDoS. Utilice limit_req_zone en Nginx para límites aproximados basados en IP (1000 solicitudes/min). Capa de limitación de velocidad a nivel de aplicación en la parte superior para un control granular por usuario y por punto final. Las dos capas se complementan entre sí: Nginx maneja ataques de volumen de manera económica sin afectar su aplicación, y NestJS maneja límites de lógica empresarial matizados.
Próximos pasos
Crear una API de producción sin una limitación de velocidad sólida es como dejar la puerta de entrada abierta. Los patrones de esta guía (depósito de tokens para tráfico en ráfagas, contador de ventana deslizante para una aplicación fluida, almacenamiento distribuido respaldado por Redis y respuestas 429 adecuadas) forman la columna vertebral de una API segura y escalable.
ECOSIRE crea backends NestJS de nivel empresarial con limitación de velocidad, almacenamiento en caché de Redis y observabilidad total desde el primer día. Si está lanzando una nueva API o fortaleciendo una existente, explore nuestros servicios de ingeniería backend para ver cómo podemos acelerar su entrega.
Escrito por
ECOSIRE Research and Development Team
Construyendo productos digitales de nivel empresarial en ECOSIRE. Compartiendo perspectivas sobre integraciones Odoo, automatización de eCommerce y soluciones empresariales impulsadas por IA.
Artículos relacionados
Cybersecurity Trends 2026-2027: Zero Trust, AI Threats, and Defense
The definitive guide to cybersecurity trends for 2026-2027—AI-powered attacks, zero trust implementation, supply chain security, and building resilient security programs.
Financial Services ERP Implementation: Regulatory and Security Requirements
A practitioner's guide to implementing ERP in regulated financial services firms, covering security controls, compliance validation, data governance, and phased rollout.
Government ERP Implementation: Security Clearance and Compliance
A comprehensive guide to implementing ERP in government agencies, covering FedRAMP compliance, security clearance requirements, change management, and phased deployment.