michangarrito/docs/97-adr/ADR-0009-rate-limiting.md
rckrdmrd 2c916e75e5 [SIMCO-V4] feat: Agregar documentación SaaS, ADRs e integraciones
Nuevas Épicas (MCH-029 a MCH-033):
- Infraestructura SaaS multi-tenant
- Auth Social (OAuth2)
- Auditoría Empresarial
- Feature Flags
- Onboarding Wizard

Nuevas Integraciones (INT-010 a INT-014):
- Email Providers (SendGrid, Mailgun, SES)
- Storage Cloud (S3, GCS, Azure)
- OAuth Social
- Redis Cache
- Webhooks Outbound

Nuevos ADRs (0004 a 0011):
- Notifications Realtime
- Feature Flags Strategy
- Storage Abstraction
- Webhook Retry Strategy
- Audit Log Retention
- Rate Limiting
- OAuth Social Implementation
- Email Multi-provider

Actualizados:
- MASTER_INVENTORY.yml
- CONTEXT-MAP.yml
- HERENCIA-SIMCO.md
- Mapas de documentación

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-13 01:43:15 -06:00

252 lines
5.3 KiB
Markdown

---
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<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:
```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