--- id: ADR-0009 type: ADR title: "Rate Limiting Strategy" status: Accepted decision_date: 2026-01-10 updated_at: 2026-01-10 simco_version: "4.0.1" stakeholders: - "Equipo MiChangarrito" tags: - 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: 1. Proteger la infraestructura 2. Garantizar uso justo entre tenants 3. Diferenciar limites por plan 4. 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:** - Preciso - Sin bursts - **Cons:** - Alto uso de memoria - Queries complejas ### Opcion 3: Token Bucket (Elegida) - **Pros:** - Permite bursts controlados - Eficiente en memoria - Smooth rate limiting - **Cons:** - Ligeramente mas complejo --- ## Consecuencias ### Positivas 1. **Proteccion:** API protegida contra abusos 2. **Justicia:** Cada tenant tiene su cuota 3. **Transparencia:** Headers informan estado 4. **Flexibilidad:** Limites por plan ### Negativas 1. **Dependencia Redis:** Requiere Redis disponible 2. **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 ```typescript @Injectable() export class RateLimitGuard implements CanActivate { constructor( @Inject('REDIS_CLIENT') private readonly redis: Redis, private readonly planService: PlanService, ) {} async canActivate(context: ExecutionContext): Promise { 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 { 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: ```typescript @RateLimitBypass() @Get('health') healthCheck() { return { status: 'ok' }; } // Decorator export const RateLimitBypass = () => SetMetadata('rateLimitBypass', true); ``` ### Response 429 ```json { "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 ```typescript 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 - [Token Bucket Algorithm](https://en.wikipedia.org/wiki/Token_bucket) - [RFC 6585 - 429 Too Many Requests](https://tools.ietf.org/html/rfc6585) - [GitHub API Rate Limiting](https://docs.github.com/en/rest/rate-limit) --- **Fecha decision:** 2026-01-10 **Autores:** Architecture Team