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>
252 lines
5.3 KiB
Markdown
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
|