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

5.3 KiB

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
Equipo MiChangarrito
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

@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