| id |
type |
title |
status |
decision_date |
updated_at |
simco_version |
stakeholders |
tags |
| ADR-0009 |
ADR |
Rate Limiting Strategy |
Accepted |
2026-01-10 |
2026-01-10 |
4.0.1 |
|
| rate-limiting |
| api |
| redis |
| security |
|
ADR-0009: Rate Limiting Strategy
Metadata
| Campo |
Valor |
| ID |
ADR-0009 |
| Estado |
Accepted |
| Fecha |
2026-01-10 |
| Autor |
Architecture Team |
| Supersede |
- |
Contexto
MiChangarrito expone una API REST que puede ser abusada por clientes mal configurados o ataques maliciosos. Necesitamos rate limiting para:
- Proteger la infraestructura
- Garantizar uso justo entre tenants
- Diferenciar limites por plan
- Informar a clientes sobre limites
Decision
Implementamos Token Bucket algorithm con Redis, limites por plan/tenant y headers estandar de rate limit.
Header: X-RateLimit-Limit: 1000
Header: X-RateLimit-Remaining: 999
Header: X-RateLimit-Reset: 1704880800
Alternativas Consideradas
Opcion 1: Fixed Window
- Pros:
- Simple de implementar
- Facil de entender
- Cons:
- Vulnerable a bursts al cambiar ventana
- No es smooth
Opcion 2: Sliding Window Log
- Pros:
- Cons:
- Alto uso de memoria
- Queries complejas
Opcion 3: Token Bucket (Elegida)
- Pros:
- Permite bursts controlados
- Eficiente en memoria
- Smooth rate limiting
- Cons:
Consecuencias
Positivas
- Proteccion: API protegida contra abusos
- Justicia: Cada tenant tiene su cuota
- Transparencia: Headers informan estado
- Flexibilidad: Limites por plan
Negativas
- Dependencia Redis: Requiere Redis disponible
- Complejidad: Logica adicional en cada request
Implementacion
Limites por Plan
| Plan |
Requests/minuto |
Requests/dia |
Burst |
| Basic |
60 |
10,000 |
10 |
| Pro |
300 |
100,000 |
50 |
| Enterprise |
1,000 |
1,000,000 |
100 |
Guard
@Injectable()
export class RateLimitGuard implements CanActivate {
constructor(
@Inject('REDIS_CLIENT') private readonly redis: Redis,
private readonly planService: PlanService,
) {}
async canActivate(context: ExecutionContext): Promise<boolean> {
const request = context.switchToHttp().getRequest();
const response = context.switchToHttp().getResponse();
const tenantId = request.user?.tenantId;
if (!tenantId) return true; // Sin tenant, skip
const plan = await this.planService.getTenantPlan(tenantId);
const limit = this.getLimitForPlan(plan);
const result = await this.checkLimit(tenantId, limit);
// Agregar headers
response.setHeader('X-RateLimit-Limit', limit.requestsPerMinute);
response.setHeader('X-RateLimit-Remaining', result.remaining);
response.setHeader('X-RateLimit-Reset', result.resetAt);
if (!result.allowed) {
response.setHeader('Retry-After', result.retryAfter);
throw new HttpException({
statusCode: 429,
message: 'Too Many Requests',
retryAfter: result.retryAfter,
}, 429);
}
return true;
}
private async checkLimit(
tenantId: string,
limit: RateLimit,
): Promise<RateLimitResult> {
const key = `rate:${tenantId}`;
const now = Date.now();
const windowMs = 60 * 1000; // 1 minuto
const pipe = this.redis.pipeline();
// Remover tokens viejos
pipe.zremrangebyscore(key, 0, now - windowMs);
// Contar tokens actuales
pipe.zcard(key);
// Agregar token actual
pipe.zadd(key, now, `${now}-${Math.random()}`);
// Expirar key
pipe.expire(key, 60);
const results = await pipe.exec();
const count = results[1][1] as number;
const allowed = count < limit.requestsPerMinute;
const remaining = Math.max(0, limit.requestsPerMinute - count - 1);
const resetAt = Math.floor((now + windowMs) / 1000);
return {
allowed,
remaining,
resetAt,
retryAfter: allowed ? 0 : Math.ceil(windowMs / 1000),
};
}
}
Bypass
Algunas rutas no tienen rate limit:
@RateLimitBypass()
@Get('health')
healthCheck() {
return { status: 'ok' };
}
// Decorator
export const RateLimitBypass = () => SetMetadata('rateLimitBypass', true);
Response 429
{
"statusCode": 429,
"message": "Too Many Requests",
"error": "Rate limit exceeded. Please retry after 45 seconds.",
"retryAfter": 45
}
Headers Estandar
| Header |
Descripcion |
X-RateLimit-Limit |
Limite maximo de requests |
X-RateLimit-Remaining |
Requests restantes |
X-RateLimit-Reset |
Unix timestamp cuando se resetea |
Retry-After |
Segundos a esperar (solo en 429) |
Monitoreo
Metricas
rate_limit_requests_total{tenant, plan, allowed}
rate_limit_exceeded_total{tenant, plan}
Alertas
- Rate limit hits > 100/minuto por tenant
- Mismo IP excediendo multiples tenants
Referencias
Fecha decision: 2026-01-10
Autores: Architecture Team