Pular para o conteúdo

ADR-0010 Limitação de pedidos

Limitação de pedidos ao nível da app, padrão da empresa — rate-limiter-flexible no middleware global de pedidos do Start, contadores no Valkey.

Decisão
rate-limiter-flexible + ValkeyGuardar e proteger
Estado
Aceite
Data
Âmbito
Predefinição da empresa para todas as novas apps

As apps precisam de proteção contra força bruta (início de sessão, reposição de palavra-passe), contra abuso de endpoints de escrita e contra clientes de API que excedem a sua quota. A identidade está definida — Better Auth (ADR-0009) — pelo que os limites podem usar como chave utilizadores e chaves de API, e não apenas IPs.

Forças em jogo:

  • Pontos de entrada: SSR, server functions e as server routes em /api/* (ADR-0036), tudo num único servidor TanStack Start (ADR-0002). O middleware global de pedidos do Start corre antes de todos
  • Topologia: uma só máquina por predefinição (ADR-0003), mas os deploys graduais executam brevemente dois processos, os reinícios apagam a memória do processo e um produto pode escalar horizontalmente
  • O Better Auth traz o seu próprio limitador para /api/auth/*, em memória por predefinição — um segundo limitador, com configuração e store próprias, se o mantivermos
  • O que um limitador na app não consegue fazer: travar uma inundação de pedidos. O servidor continua a aceitar todas as ligações e a executar middleware antes de as rejeitar. A proteção contra inundações pertence à frente da app — o reverse proxy (ADR-0017)

Prós: idiomático para a camada de API; pequeno. Contras: o Hono nunca vê server functions nem SSR — um formulário de início de sessão feito com server function ficaria desprotegido.

2. O limitador próprio de cada biblioteca (o do Better Auth + Hono + …)

Seção intitulada “2. O limitador próprio de cada biblioteca (o do Better Auth + Hono + …)”

Prós: predefinições específicas por endpoint sem esforço. Contras: várias configurações, várias stores, vários formatos de 429; ninguém tem a visão de conjunto.

3. Um só limitador no middleware global de pedidos do Start (rate-limiter-flexible)

Seção intitulada “3. Um só limitador no middleware global de pedidos do Start (rate-limiter-flexible)”

Prós

  • Um único ponto de controlo para todos os pontos de entrada: SSR, server functions, server routes, /api/*
  • rate-limiter-flexible: agnóstico de framework, sem dependências, ISC, ativamente mantido; stores para memória, Postgres, Drizzle e Valkey
  • Fallback em memória quando a store está em baixo (insuranceLimiter); as chaves que já excederam o limite são bloqueadas em memória sem tocar na store (inMemoryBlockOnConsumed)

Contras

  • Passamos a manter as regras dos endpoints de autenticação que o Better Auth forneceria

Prós: zero infraestrutura, o mais rápido. Contras: por processo — os contadores são repostos a cada deploy e dividem-se assim que existe uma segunda réplica.

Prós: sem nova infraestrutura; partilhado e durável. Contras: uma escrita por pedido limitado no nosso sistema de registo; a contenção durante um ataque recai sobre a base de dados de que a app mais precisa.

Prós

  • Feito para isto: contadores atómicos em memória com TTL, na escala dos microssegundos
  • Mantém a carga de escrita do rate limiting fora do Postgres (ADR-0007)
  • Fork do Redis com licença BSD, sob a Linux Foundation; fala o protocolo Redis, pelo que os clientes Redis funcionam
  • Disponível para ADRs futuros (cache, filas, secondary storage do Better Auth) assim que existir

Contras

  • Um novo serviço por ambiente para executar e monitorizar

rate-limiter-flexible no middleware global de pedidos do Start, com os contadores no Valkey, é a limitação de pedidos padrão da empresa para todas as novas apps.

Em concreto:

  • Um só limitador para tudo. O limitador integrado do Better Auth está desativado (rateLimit: { enabled: false }); o nosso middleware cobre também /api/auth/*

  • Âmbito: todos os pedidos que não sejam GET/HEAD (escritas por server function, submissões de formulários), todos os pedidos a /api/* independentemente do método e todos os endpoints de autenticação. Os carregamentos de página e as server functions GET não são limitados na app — as inundações são trabalho do proxy

  • Chaves: ID do utilizador quando existe sessão, chave de API para clientes de API, IP do cliente nos restantes casos. O IPv6 usa como chave o prefixo /64. O IP do cliente vem apenas do único cabeçalho de confiança que o edge garante (CLIENT_IP_HEADER: X-Real-Ip, definido pelo Traefik a partir da ligação TCP — ADR-0017) — nunca de uma cadeia X-Forwarded-For fornecida pelo cliente — e a porta da app nunca é acessível a não ser através do edge

  • Os endpoints de credenciais (início de sessão, criação de conta, reposição de palavra-passe, verificação 2FA, envio de magic links e de emails de verificação) têm limites estritos, com chave por IP e por email normalizado, para que o credential stuffing espalhado por muitos IPs continue a bater num muro. A lista de caminhos vive numa só configuração junto da configuração de autenticação e é revista em cada upgrade do Better Auth

  • Limites iniciais (ajustar por produto):

    Âmbito Chave Limite
    Endpoints de credenciais IP 3 / 10 s
    Endpoints de credenciais email 10 / 15 min
    Escritas e /api/* IP 100 / 60 s
    Escritas e /api/* utilizador 300 / 60 s
    /api/* com chave de API chave de API conforme o contrato do produto
  • Valkey: imagem oficial valkey/valkey, com major fixada (9.x), como contentor irmão, tal como o Postgres. Serve também de store de sessões do Better Auth (ADR-0009). Persistência AOF e maxmemory-policy noeviction, porque as filas de jobs também vivem aqui (ADR-0028); os contadores continuam descartáveis e as sessões mantêm a cópia durável no Postgres. (Alterado em 2026-11-12: este ADR correu primeiro o Valkey sem persistência.) Protegido por palavra-passe e acessível apenas na rede Docker interna — guarda tokens de sessão ativos

  • Cliente: uma única ligação ioredis 5.x partilhada pelo limitador e pelo @better-auth/redis-storage do Better Auth (que exige ioredis ^5). JS puro. O cliente Redis integrado no Bun não é opção aqui: o rate-limiter-flexible e o @better-auth/redis-storage aceitam apenas os clientes que listam, e um só cliente para o Valkey vale mais do que dois (ADR-0035)

  • Modo de falha: se o Valkey estiver inacessível, o insuranceLimiter em processo assume — os limites mantêm-se ativos, por processo, até o Valkey voltar. As consultas de sessão recorrem ao Postgres

  • Resposta: 429 Too Many Requests com um cabeçalho Retry-After (segundos), com a mesma forma em todos os pontos de entrada

Os fatores decisivos: um único ponto de controlo e uma só configuração para todos os pontos de entrada, contadores que sobrevivem a reinícios e se mantêm corretos entre réplicas, e a carga de escrita do rate limiting fora do Postgres. Aceitamos, em troca, operar o Valkey e ser donos das regras dos endpoints de autenticação.

Mais fácil:

  • Um único sítio para ler, ajustar e testar todos os limites de uma app
  • Correto entre deploys e réplicas desde o primeiro dia — escalar horizontalmente não exige retrabalho do limitador
  • O Valkey já está a correr quando ADRs futuros (cache, filas) o quiserem; as sessões já o usam (ADR-0009)

Mais difícil:

  • O Valkey é mais um contentor por ambiente: upgrades de imagem, health checks, dimensionamento de memória
  • Somos donos das regras dos endpoints de credenciais; um upgrade do Better Auth que acrescente ou renomeie endpoints de autenticação tem de atualizar a lista, ou o novo endpoint cai no limite genérico de escritas
  • Ler o corpo do pedido para usar o email como chave implica clonar o pedido nos endpoints de credenciais — limitar isto a esses caminhos
  • Definir src/start.ts para o limitador desativa o middleware CSRF automático do Start para server functions; registar aí também createCsrfMiddleware() (ADR-0013)
  • Os IPs de cliente corretos dependem do edge (ADR-0017): o Traefik tem de continuar a ser o primeiro salto, sem forwarders de confiança configurados, ou o cabeçalho do IP do cliente pode ser forjado

Do que abdicamos: das predefinições por endpoint que o Better Auth mantém e dos contadores em memória sem infraestrutura. Aceite — um limitador com estado partilhado vale mais do que dois parciais, e o Valkey justifica o seu contentor. A proteção contra inundações fica explicitamente fora do âmbito deste ADR; o reverse proxy trata do que uma só máquina consegue (ADR-0017).