SuperdindaDocs

Limites de taxa

Quais rotas têm limite, as janelas e os números de cada uma, a resposta 429 RATE_LIMITED com Retry-After e como o contador funciona no Redis.

O limite protege as rotas públicas: as que mandam e-mail, as que chamam um verificador pago e as que só têm o token do link como credencial. As rotas com Bearer não têm limite de taxa hoje.

A resposta

HTTP/1.1 429 Too Many Requests
Retry-After: 2731
content-type: application/json

{ "success": false,
  "error": { "code": "RATE_LIMITED", "message": "too many requests",
             "details": { "retryAfterSeconds": 2731 } } }

Retry-After e details.retryAfterSeconds sempre têm o mesmo valor: os segundos que faltam pra janela acabar. O header é posto pelo errorHandler sempre que o erro é um TooManyRequestsError.

Rotas com limite

Cada regra dá um orçamento pelo identificador (e-mail, link ou token) e outro por IP. As duas contam, e a primeira que estourar responde 429. O formato da variável é limit/windowSeconds.

Login

RotaPor identificadorPor IP
POST /auth/otp/requestRATE_OTP_REQUEST_PER_EMAIL = 5/3600. Conta também endereço sem conta. Maiúsculas e espaços são ignoradosRATE_OTP_REQUEST_PER_IP = 20/3600 (balde mail)
POST /auth/otp/resendRATE_OTP_RESEND_PER_LINK = 3/3600, mais o orçamento do e-mail (o mesmo do request)O mesmo balde mail
POST /auth/otp/verifyO código trava depois de OTP_MAX_ATTEMPTS (5) erros (OTP_LOCKED, não 429)RATE_OTP_VERIFY_PER_IP = 40/3600

Link de consentimento (por link e por IP)

RotaPor linkPor IP
POST /consent-requests/{token}/evidenceRATE_EVIDENCE_PER_LINK = 10/3600RATE_EVIDENCE_PER_IP = 30/3600
…/decline e …/stop (um balde só)RATE_CONSENT_WRITE_PER_LINK = 10/3600RATE_CONSENT_WRITE_PER_IP = 30/3600
GET /consent-requests/{token} e …/protection (um balde só)RATE_CONSENT_READ_PER_LINK = 120/600RATE_CONSENT_READ_PER_IP = 600/600

Pareamento pelo celular da pessoa (números fixos no código, janela de 10 min)

As regras de uma hora lado a lado, com os padrões do env.ts:

Pedidos permitidos por janela de 1 hora
  • Por identificador (e-mail ou link)
  • Por IP
Pedidos permitidos por janela de 1 hora
regraPor identificador (e-mail ou link)Por IP
Pedir código520
Evidência1030
Recusar ou parar1030

Padrões de backend/src/config/env.ts: RATE_OTP_REQUEST_PER_EMAIL/_PER_IP, RATE_EVIDENCE_PER_LINK/_PER_IP, RATE_CONSENT_WRITE_PER_LINK/_PER_IP.

Algumas escolhas:

  • A evidência é limitada antes de ler o corpo. Um pedido recusado não custa o parse de um multipart grande.
  • Nas rotas de consentimento, o limite roda antes de procurar o token. Testar tokens em sequência custa o mesmo que insistir num só.
  • O orçamento por IP é mais folgado porque uma família costuma dividir o mesmo roteador.

Implementação

backend/src/infra/rate-limit.ts é uma janela fixa no Redis:

  1. INCR ratelimit:<chave>;
  2. no primeiro acerto da janela, EXPIRE com windowSeconds;
  3. passou do limit: lê o TTL e lança TooManyRequestsError(ttl). Se a chave ficou sem expiração (queda entre o INCR e o EXPIRE), ela ganha uma ali mesmo, pra ninguém ficar travado pra sempre.

É grosseira de propósito: o que importa é "mais ou menos tantos por janela". As chaves nunca guardam segredo em claro. Usam o SHA-256 do e-mail ou do token (otp:request:email:<sha>, consent:read:link:<hash>, pairing:start:link:<hash>) e o IP (…:ip:<ip>).

Limitando uma rota nova
import { rateLimit } from "@infra/rate-limit.ts";
import { clientIp } from "../../request-meta.ts";

await rateLimit(`minha-rota:link:${hashToken(token)}`, {
  limit: 10,
  windowSeconds: 600,
});
await rateLimit(`minha-rota:ip:${clientIp(c)}`, {
  limit: 30,
  windowSeconds: 600,
});
// passou do limite → 429 RATE_LIMITED + Retry-After, pelo errorHandler

No cliente, use retryAfterSeconds pra mostrar a espera ("Tente de novo em N min") e pra controlar o botão de reenviar.

Ver o trecho de um alerta (POST /alerts/{uid}/excerpt) tem limite por responsável: 30 por minuto, chave alerts:excerpt:user:{id}. É a leitura mais sensível do sistema; o 429 traz o mesmo retryAfterSeconds.

Em aberto

  • clientIp() confia na primeira entrada do X-Forwarded-For. Isso só está certo se o proxy de borda (Traefik no Dokploy) descarta o header que o cliente manda. Se api.superdinda.com ficar atrás da Cloudflare, os baldes por IP passam a juntar todo mundo nos IPs dela, e o header pode ser forjado. Falta conferir a cadeia de produção.

Fontes

  • backend/src/infra/rate-limit.ts, backend/src/http/request-meta.ts
  • backend/src/http/routes/auth/limits.ts, backend/src/domains/auth/use-cases/request-otp.ts, resend-link.ts, code-lock.ts
  • backend/src/http/routes/consents/limits.ts, pairing.route.ts, page.route.ts, evidence.route.ts, decline.route.ts, protection.route.ts
  • backend/src/http/middleware/error-handler.ts, backend/src/core/contracts/errors.ts
  • backend/src/config/env.ts, backend/.env.example
  • Notas das frentes k-otp e k-pairing (k-otp-api.md, k-pairing-api.md)
  • .specs/features/redesign/api-gaps.md (G-04, G-14)

On this page