Para grandes projetos, geralmente é melhor usar ferramentas como Cloudflare Rate Limiting ou HAProxy. Eles são poderosos, confiáveis e cuidam do trabalho pesado para você.
Mas para projetos menores – ou se você quiser aprender como as coisas funcionam – você pode criar seu próprio limitador de taxa diretamente em seu código. Por que?
Ao final deste guia, você saberá como construir um acelerador básico em TypeScript para proteger suas APIs contra sobrecarga. Aqui está o que abordaremos:
Este guia foi projetado para ser um ponto de partida prático, perfeito para desenvolvedores que desejam aprender o básico sem complexidade desnecessária. Mas não está pronto para produção.
Antes de começar, quero dar os devidos créditos à seção Rate Limiting de Lucia.
Vamos definir a classe Throttler:
export class Throttler { private storage = new Map(); constructor(private timeoutSeconds: number[]) {} }
O construtor Throttler aceita uma lista de durações de tempo limite (timeoutSeconds). Cada vez que um usuário é bloqueado, a duração aumenta progressivamente com base nesta lista. Eventualmente, quando o tempo limite final for atingido, você poderá até acionar um retorno de chamada para banir permanentemente o IP do usuário – embora isso esteja além do escopo deste guia.
Aqui está um exemplo de criação de uma instância de acelerador que bloqueia usuários em intervalos crescentes:
const throttler = new Throttler([1, 2, 4, 8, 16]);
Esta instância bloqueará os usuários pela primeira vez por um segundo. A segunda vez para dois e assim por diante.
Usamos um mapa para armazenar endereços IP e seus dados correspondentes. Um mapa é ideal porque lida com adições e exclusões frequentes com eficiência.
Dica profissional: use um mapa para dados dinâmicos que mudam com frequência. Para dados estáticos e imutáveis, um objeto é melhor. (Toca de coelho 1)
Quando seu endpoint recebe uma solicitação, ele extrai o endereço IP do usuário e consulta o Throttler para determinar se a solicitação deve ser permitida.
Caso A: usuário novo ou inativo
Se o IP não for encontrado no Throttler, é a primeira solicitação do usuário ou ele está inativo há tempo suficiente. Nesse caso:
Caso B: usuário ativo
Se o IP for encontrado, significa que o usuário já fez solicitações anteriores. Aqui:
Neste último caso, precisamos verificar se passou tempo suficiente desde o último bloco. Sabemos qual dos timeoutSeconds devemos nos referir graças a um índice. Se não, simplesmente se recupere. Caso contrário, atualize o carimbo de data/hora.
export class Throttler { // ... public consume(key: string): boolean { const counter = this.storage.get(key) ?? null; const now = Date.now(); // Case A if (counter === null) { // At next request, will be found. // The index 0 of [1, 2, 4, 8, 16] returns 1. // That's the amount of seconds it will have to wait. this.storage.set(key, { index: 0, updatedAt: now }); return true; // allowed } // Case B const timeoutMs = this.timeoutSeconds[counter.index] * 1000; const allowed = now - counter.updatedAt >= timeoutMs; if (!allowed) { return false; // denied } // Allow the call, but increment timeout for following requests. counter.updatedAt = now; counter.index = Math.min(counter.index 1, this.timeoutSeconds.length - 1); this.storage.set(key, counter); return true; // allowed } }
Ao atualizar o índice, ele limita ao último índice de timeoutSeconds. Sem ele, counter.index 1 iria estourá-lo e o próximo acesso this.timeoutSeconds[counter.index] resultaria em um erro de tempo de execução.
Este exemplo mostra como usar o Throttler para limitar a frequência com que um usuário pode chamar sua API. Se o usuário fizer muitas solicitações, receberá um erro em vez de executar a lógica principal.
const throttler = new Throttler([1, 2, 4, 8, 16, 30, 60, 300]); export async function GET({ getClientAddress }) { const IP = getClientAddress(); if (!throttler.consume(IP)) { throw error(429, { message: 'Too Many Requests' }); } // Read from DB, call OpenAI - do the thing. return new Response(null, { status: 200 }); }
Ao usar a limitação de taxa com sistemas de login, você pode enfrentar este problema:
Para evitar isso, use o ID de usuário exclusivo do usuário em vez de seu IP para limitação de taxa. Além disso, você deve redefinir o estado do acelerador após um login bem-sucedido para evitar bloqueios desnecessários.
Adicione um método de redefinição à classe Throttler:
export class Throttler { // ... public reset(key: string): void { this.storage.delete(key); } }
E use-o após um login bem-sucedido:
const user = db.get(email); if (!throttler.consume(user.ID)) { throw error(429); } const validPassword = verifyPassword(user.password, providedPassword); if (!validPassword) { throw error(401); } throttler.reset(user.id); // Clear throttling for the user
À medida que seu acelerador rastreia IPs e limites de taxa, é importante pensar em como e quando remover registros de IP que não são mais necessários. Sem um mecanismo de limpeza, seu acelerador continuará armazenando registros na memória, potencialmente levando a problemas de desempenho ao longo do tempo, à medida que os dados crescem.
Para evitar isso, você pode implementar uma função de limpeza que remove periodicamente registros antigos após um determinado período de inatividade. Aqui está um exemplo de como adicionar um método de limpeza simples para remover entradas obsoletas do acelerador.
export class Throttler { // ... public cleanup(): void { const now = Date.now() // Capture the keys first to avoid issues during iteration (we use .delete) const keys = Array.from(this.storage.keys()) for (const key of keys) { const counter = this.storage.get(key) if (!counter) { // Skip if the counter is already deleted (handles concurrency issues) return } // If the IP is at the first timeout, remove it from storage if (counter.index == 0) { this.storage.delete(key) continue } // Otherwise, reduce the timeout index and update the timestamp counter.index -= 1 counter.updatedAt = now this.storage.set(key, counter) } } }
Uma maneira muito simples (mas provavelmente não a melhor) de agendar a limpeza é com setInterval:
const throttler = new Throttler([1, 2, 4, 8, 16, 30, 60, 300]) const oneMinute = 60_000 setInterval(() => throttler.cleanup(), oneMinute)
Esse mecanismo de limpeza ajuda a garantir que seu acelerador não retenha registros antigos indefinidamente, mantendo seu aplicativo eficiente. Embora esta abordagem seja simples e fácil de implementar, ela pode precisar de mais refinamento para casos de uso mais complexos (por exemplo, usando agendamento mais avançado ou lidando com alta simultaneidade).
Com a limpeza periódica, você evita o excesso de memória e garante que os usuários que não tentam fazer solicitações há algum tempo não sejam mais rastreados - este é o primeiro passo para tornar seu sistema de limitação de taxa escalonável e eficiente em termos de recursos.
Se você estiver se sentindo aventureiro, pode estar interessado em ler como as propriedades são alocadas e como isso muda. Além disso, por que não, sobre otimizações de VMs, como caches inline, que são particularmente favorecidos pelo monomorfismo. Aproveitar. ↩
Isenção de responsabilidade: Todos os recursos fornecidos são parcialmente provenientes da Internet. Se houver qualquer violação de seus direitos autorais ou outros direitos e interesses, explique os motivos detalhados e forneça prova de direitos autorais ou direitos e interesses e envie-a para o e-mail: [email protected]. Nós cuidaremos disso para você o mais rápido possível.
Copyright© 2022 湘ICP备2022001581号-3