Identificando clientes
Um limite de cinco requisições por minuto não significa nada até você dizer cinco por o quê.
O rate limiting só funciona se você conseguir identificar de forma consistente quem está fazendo a requisição, e o identificador escolhido decide quatro coisas ao mesmo tempo:
- Justiça. Você está limitando a entidade certa?
- Eficácia. Alguém consegue driblar o limite?
- Experiência. Usuários legítimos estão sendo penalizados?
- Segurança. O próprio identificador pode ser explorado de forma indevida?
A pergunta por trás de todo rate limit: quem ou o que você está realmente tentando limitar?
Endereço IP
O padrão, e o que você obtém sem nem pedir. O express-rate-limit usa o endereço do cliente a menos que você diga o contrário, que é exatamente o que o limitador do capítulo anterior vinha fazendo.
Bom para endpoints públicos sem login, prevenção básica de abuso e conter enchentes simples de requisições.
O atrativo é que ele sempre existe. Não precisa de autenticação, funciona para tráfego anônimo, é simples, e um usuário comum não consegue mudá-lo por capricho.
O problema é que endereços são compartilhados e endereços se movem. Centenas de funcionários atrás de uma única conexão de escritório parecem um único cliente, assim como todo mundo em uma biblioteca e um grande número de usuários móveis atrás de uma operadora.
Enquanto isso, um usuário com endereço variável ganha um orçamento novo cada vez que ele muda, e o IPv6 distribui muitos endereços dentro de uma única sub-rede.
Tornando a escolha explícita em vez de simplesmente herdá-la:
import { rateLimit, ipKeyGenerator } from 'express-rate-limit'
const limiter = rateLimit({
limit: 5,
windowMs: 60000,
keyGenerator: (req, res) => ipKeyGenerator(req.ip),
})keyGenerator retorna a string contra a qual uma requisição é contabilizada. Esse é o gancho que tudo o que vem a seguir utiliza.
Todo problema dessa estratégia vem dessa diferença.
req.ip é o endereço do proxy, então toda requisição parece vir de um único cliente e seu limite acaba se aplicando ao mundo inteiro de uma vez. O Express precisa de app.set('trust proxy', ...) para ler o endereço encaminhado em vez disso. Configure com o número de proxies que você realmente opera, porque confiar cegamente no cabeçalho permite que um cliente falsifique qualquer endereço que quiser e driblar o limite por completo.
A resposta é limitar por prefixo. O express-rate-limit 8 expõe ipv6Subnet para isso, com valor padrão 56, onde números menores agrupam de forma mais agressiva.
O auxiliar ipKeyGenerator existe pelo mesmo motivo: ele normaliza os endereços para que clientes IPv4 e IPv6 sejam identificados de forma consistente. Escrever req.ip diretamente na chave é o que silenciosamente para de funcionar no dia em que chega tráfego IPv6.
ID do usuário autenticado
Depois que alguém faz login, você sabe exatamente quem é essa pessoa, seja por uma sessão, uma claim de JWT ou uma consulta ao banco de dados.
Bom para endpoints autenticados, cotas por usuário e planos escalonados.
Cada usuário real tem seu próprio orçamento, então ninguém interfere no limite de ninguém. Esse identificador acompanha o usuário entre dispositivos e redes, e trocar de endereço não muda nada, porque a identidade não é a conexão.
O que ele não consegue fazer é proteger nada antes do login. Cadastro, redefinição de senha e endpoints públicos acontecem todos antes de existir um id de usuário, e são exatamente esses os endpoints que o abuso costuma encontrar.
const limiter = rateLimit({
limit: 3,
windowMs: 10000,
keyGenerator: (req) => req.user.id,
})
app.get('/api/data', authenticate, limiter, handler)A ordem importa. O Express executa os middlewares da esquerda para a direita, então a autenticação precisa definir req.user antes que o limitador o leia. Coloque o limitador primeiro e ele vai ler undefined para todo mundo, associando todas as requisições ao mesmo grupo.
Isso é justiça funcionando. Com endereços, três pessoas no mesmo escritório estariam compartilhando um único orçamento de três.
req.user.id antes de a autenticação rodar vai lançar um erro por causa do undefined ou vai associar todo mundo ao mesmo valor, o que resulta em um único limite compartilhado por toda a sua base de usuários. Nenhum dos dois problemas aparece em um teste de caminho feliz, porque com um único usuário no teste não há diferença visível.
Os limites também passam a fazer parte do produto quando são por usuário. Planos gratuitos e pagos com orçamentos diferentes exigem que o limite seja uma função da requisição, não uma constante, e a consulta ao plano do usuário passa a ficar no caminho crítico de cada requisição. Faça cache disso.
Chave de API e ID de sessão
Mais dois identificadores, cada um adequado a um tipo diferente de chamador.
| Chave de API | ID de sessão | |
|---|---|---|
| Bom para | Integrações de terceiros, APIs corporativas, planos escalonados | Aplicações web com sessões, carrinhos, checkout e fluxos de formulário |
| Pontos fortes | Vincula-se a uma aplicação específica, suporta níveis de plano, é revogável em caso de abuso, simples de rastrear | Funciona tanto para usuários logados quanto para visitantes, mais persistente que um endereço, acompanha toda uma jornada |
| Pontos fracos | Chaves são compartilhadas e vazam, uma única chave pode atender milhares de usuários finais, e exige gerenciamento de chaves | Limpar os cookies reinicia a sessão, a fixação de sessão é um risco, e ela simplesmente não existe em APIs sem estado |
Uma chave de API torna o escalonamento por níveis natural, porque limit pode ser uma função da requisição:
import { getUserTier } from './services/billing.js'
const limiter = rateLimit({
windowMs: 60000,
keyGenerator: (req) => req.headers['x-api-key'],
limit: (req) => (getUserTier(req.headers['x-api-key']) === 'pro' ? 1000 : 100),
})Um id de sessão é lido da mesma forma, a partir de req.session.id.
Uma chave de API identifica uma aplicação. Um id de usuário identifica uma pessoa. Uma sessão identifica uma visita. Um endereço identifica uma conexão. Escolha o que combina com o que você está limitando.
Um parceiro com cinquenta mil usuários finais tem uma única chave, então um único usuário problemático consome o orçamento de todos e faz a integração inteira ser limitada.
Se isso importa, a chave precisa de um orçamento por chave mais um orçamento por usuário final dentro dela, o que significa repassar um identificador de usuário final através da integração.
Duas coisas valem a pena independentemente do que você usa como chave. Nunca coloque um segredo bruto na chave, já que chaves de limitador acabam parando em logs e dumps de armazenamento, então faça hash da chave de API antes.
E decida o que acontece quando o identificador está ausente, porque um keyGenerator que retorna undefined agrupa todas essas requisições em um único grupo. Isso tanto pode ser um catch-all útil quanto um limite compartilhado por acidente, e a decisão deveria ser sua.
Combinando os identificadores
Sistemas reais raramente escolhem apenas um. O arranjo mais comum é uma cadeia com alternativas de reserva, do mais específico para o menos específico:
- ID do usuário autenticado, quando alguém está logado.
- Chave de API, para integrações.
- ID de sessão, para visitantes com uma sessão.
- Endereço IP, como último recurso.
Cada passo abaixo é menos preciso e mais disponível, então uma requisição é sempre contabilizada contra o melhor identificador que ela realmente possui.
Uma pessoa logada é a coisa mais clara que você pode identificar. Um endereço é a mais vaga, e a única que está sempre presente.
Muitas vezes é melhor rodar limitadores separados, cada um com seu próprio orçamento: um mais restrito para tráfego anônimo, um mais generoso para usuários autenticados, em vez de um único limitador trocando de chave.
user:4821, nunca um 4821 puro. Sem isso, um id de usuário e um id de sessão podem colidir na mesma string e dois clientes sem relação nenhuma acabam compartilhando um orçamento. A outra coisa que a cadeia esconde é que ela é um caminho de escalonamento: um atacante opera no nível que for mais barato de obter.
Então a cadeia é só tão forte quanto seu elo mais fraco, e os limites deveriam ficar mais rígidos conforme os identificadores se tornam mais anônimos, em vez de permanecerem uniformes ao longo da lista.
Coloque em prática
Uma API limita 100 requisições por minuto por endereço IP. Chegam três reclamações.
- Uma empresa com 400 funcionários diz que o aplicativo para de funcionar toda tarde.
- Um usuário no celular diz que o limite parece nunca se aplicar a ele.
- Alguém está fazendo scraping do catálogo público a uma taxa de aproximadamente 10.000 requisições por hora, e nada está impedindo isso.
Compare suas respostas
| # | O que está acontecendo | Correção |
|---|---|---|
| 1 | 400 pessoas compartilham um único endereço de saída, então 400 pessoas dividem 100 requisições por minuto | Usar como chave o id do usuário autenticado, mantendo o limite por endereço apenas para tráfego anônimo |
| 2 | A operadora move o usuário entre endereços, então cada troca dá a ele um orçamento novo | A mesma correção. Uma identidade que acompanha a pessoa não pode ser descartada só por reconectar |
| 3 | O catálogo é público, então não há usuário para usar como chave, e endereços custam quase nada para quem faz o scraping | Limites anônimos mais rígidos, usando como chave um prefixo IPv6 em vez de um único endereço, além de alguma proteção anterior no fluxo. Esse caso é reduzido, não resolvido |
Um e dois são o mesmo defeito visto de dois ângulos diferentes: o endereço é grosseiro demais para o escritório e fino demais para o usuário móvel.
Três é o limite honesto de toda a técnica. O rate limiting faz o abuso custar alguma coisa, e contra um endpoint público esse preço nunca é maior do que o custo do seu identificador mais barato.
Para onde isso vai a seguir
O limitador conta corretamente e contra a coisa certa. O que ainda resta é o pico na fronteira da janela fixa, que deixa passar o dobro do limite em torno de um reset.
Algoritmos de janela deslizante resolve isso, medindo contra uma janela que se move junto com a requisição em vez de um bloco fixo no relógio.

