Limitar pedidos sem incomodar quem visita
Um formulário aberto na internet acaba por receber a visita de um robô. Sem limite nenhum, uma tarde chega para encher a base de dados — e o custo de a limpar é sempre maior do que o de a proteger.
1. Janela fixa: simples, com um defeito
const contagens = new Map();
function dentroDoLimite(chave, maximo, janelaMs) {
const agora = Date.now();
const registo = contagens.get(chave);
if (!registo || agora > registo.expiraEm) {
contagens.set(chave, { total: 1, expiraEm: agora + janelaMs });
return true;
}
if (registo.total >= maximo) return false;
registo.total++;
return true;
}O defeito está na fronteira: com um limite de 5 por minuto, alguém pode fazer 5 no último segundo de um minuto e 5 no primeiro do seguinte — 10 em dois segundos. Para um formulário de comentários é irrelevante. Para uma API de pagamentos, não.
2. Janela deslizante
const pedidos = new Map(); // chave → lista de instantes
function dentroDoLimite(chave, maximo, janelaMs) {
const agora = Date.now();
const recentes = (pedidos.get(chave) ?? []).filter((t) => agora - t < janelaMs);
if (recentes.length >= maximo) return false;
recentes.push(agora);
pedidos.set(chave, recentes);
return true;
}Guarda os instantes e conta os que ainda contam. Gasta mais memória, e não tem fronteiras a explorar.
3. Responder como deve ser
res.status(429).set('Retry-After', '60').json({
erro: 'Demasiados pedidos. Tente daqui a um minuto.',
});O código é o 429, e o Retry-After diz ao cliente quando pode voltar. Um cliente bem feito espera; sem esse cabeçalho, tenta outra vez já.
4. Os dois avisos que interessam
Em memória só funciona com um processo. Com duas instâncias atrás de um balanceador, cada uma conta a sua metade e o limite real é o dobro. A partir daí, é preciso um sítio partilhado — Redis, tipicamente.
A chave tem de ser a certa. Por IP apanha o robô e apanha também um escritório inteiro atrás do mesmo IP. Onde há sessão, limite-se por utilizador; onde não há, por IP, e com folga suficiente para uma pessoa normal nunca lá chegar.