“Cem requisições por minuto” é uma regra fácil de configurar e difícil de avaliar sem saber o que cada chamada faz. Consultar o andamento de uma exportação pode exigir uma leitura pequena; gerar um relatório com meses de dados ocupa workers e pode acionar um serviço cobrado por uso. Tratar as duas operações com a mesma cota esconde essa diferença.
Um rate limit útil precisa identificar quem consome, qual operação está sendo executada e qual recurso deve ser protegido. A OWASP trata o consumo irrestrito de recursos em APIs como um problema que envolve processamento, armazenamento, tamanho das operações e serviços cobrados por uso. A política precisa alcançar esse custo, inclusive quando o consumidor respeita a frequência permitida.
Comece pelo recurso que não pode acabar
Para proteger a geração de relatórios, há três perguntas diferentes. Quantas operações podem começar em um intervalo? Quantas podem permanecer em execução ao mesmo tempo? Quantas unidades de trabalho uma empresa pode consumir durante um período maior?
O limite de frequência controla a entrada. O de concorrência controla o trabalho simultâneo. O orçamento controla uma quantidade acumulada, como páginas processadas ou créditos de uma integração. Uma regra por minuto não substitui as outras duas. Se cada relatório demora vários minutos, um fluxo aparentemente pequeno de novas chamadas ainda consegue ocupar todos os workers.
Combinar um teto de exportações simultâneas por empresa com um orçamento diário de processamento permite controlar a ocupação dos workers e o consumo acumulado. Os valores dependem da capacidade, da duração das tarefas e do comportamento real do produto. A rota de consulta do andamento precisa de uma política própria, para que a interface não dispute a mesma cota usada para criar relatórios.
Também é necessário limitar o trabalho dentro de cada chamada: intervalo máximo de datas, quantidade de registros, tamanho da resposta e tempo de execução. Dez chamadas sem limites internos podem custar mais que milhares de leituras pequenas. Estimar o custo de uma operação não dispensa estabelecer um teto verificável para o que ela pode fazer.
A chave do limite representa quem compartilha o orçamento
O IP ajuda a aplicar uma barreira inicial antes da autenticação, especialmente contra tráfego volumoso e tentativas repetidas. Ele não identifica de forma estável uma pessoa ou uma empresa. Redes corporativas e móveis podem compartilhar endereços; um consumidor também pode mudar de rede ou usar vários endereços.
Depois da autenticação, a API pode construir chaves por usuário, organização e classe de operação. Uma chave como organizacao:42:exportacao faz os membros daquela empresa dividirem a cota do recurso caro. Uma regra adicional por usuário pode evitar que uma única pessoa consuma toda a parcela da organização.
O identificador vem do contexto autenticado e autorizado no servidor. Aceitar um userId ou tenantId arbitrário enviado no corpo como identidade do contador permite que o chamador troque de cota. A rota também deve ser normalizada: uma classe como “criar exportação” é mais previsível do que usar a URL completa, com parâmetros que criam milhares de chaves distintas.
Quando há proxies, o endereço observado depende da cadeia de confiança configurada. A aplicação não deve aceitar qualquer header encaminhado como prova do IP original. A documentação do Express sobre proxies é um exemplo de como essa configuração muda os dados considerados confiáveis; outras stacks exigem conferir seu próprio contrato.
No fim, as camadas podem coexistir: uma barreira geral na borda, limites por identidade após autenticação e um teto global da operação. É preciso decidir se uma tentativa recusada também consome algum desses contadores. Ela não gerou um relatório, mas ainda consumiu recursos para ser recebida e avaliada.
A janela escolhida muda a forma das rajadas
Uma janela fixa conta operações durante um período e reinicia o contador no próximo. É simples de explicar, mas permite concentração perto da mudança de janela: um consumidor pode gastar a cota no final de uma e novamente no começo da seguinte. Um limite de dez por minuto não garante espaçamento uniforme entre chamadas.
Uma janela deslizante considera um intervalo móvel. Registrar cada evento permite precisão, com custo de memória e manutenção proporcional ao volume. Usar contadores por fatias aproxima o resultado com menos estado, mas essa aproximação precisa fazer parte do contrato.
O token bucket oferece outra escolha. Um saldo recebe unidades a uma taxa definida, até uma capacidade máxima. Cada operação consome parte do saldo. Isso permite uma rajada limitada e controla a reposição ao longo do tempo. Com capacidade de vinte unidades e reposição de duas por segundo, o saldo cheio admite vinte unidades de uma vez; durante dez segundos, pode receber outras vinte, conforme o momento das chamadas.
Se uma leitura custa uma unidade e uma exportação custa dez, ambas podem usar o mesmo mecanismo com pesos diferentes. Esses pesos são uma decisão do backend. Se o custo real varia muito, a aplicação precisa limitar os parâmetros ou reservar uma estimativa conservadora, em vez de confiar que todas as exportações terão o mesmo tamanho.
A escolha do algoritmo vem depois da política. Uma integração que dispara pequenos lotes pode precisar de rajadas. Uma operação que disputa uma dependência sensível pode exigir espaçamento mais regular. Nenhum algoritmo elimina a necessidade de limitar concorrência ou de separar consumidores que não deveriam disputar a mesma cota.
O contador precisa funcionar com todas as instâncias
Um contador em memória limita apenas o processo que o mantém. Se a aplicação ganha quatro instâncias independentes, um consumidor pode encontrar quatro parcelas da mesma cota. Para um limite compartilhado, as instâncias precisam consultar um estado comum ou aceitar explicitamente uma aproximação local.
Além de compartilhar o estado, é necessário coordenar a atualização. Fazer “ler contador, comparar e depois incrementar” permite que chamadas concorrentes leiam o mesmo saldo e sejam aceitas juntas. A operação que consome a cota precisa produzir uma decisão indivisível para aquele estado.
Para uma janela simples em Redis, este script ilustra incremento e expiração na mesma execução:
local count = redis.call('INCR', KEYS[1])
if count == 1 then
redis.call('PEXPIRE', KEYS[1], ARGV[1])
end
local remainingMs = redis.call('PTTL', KEYS[1])
return { count, remainingMs }
A chave é construída pelo servidor. ARGV[1] contém uma duração positiva, em milissegundos, definida pela política. O chamador compara o contador retornado com o limite antes de iniciar o trabalho. A janela começa na primeira tentativa e, nesse exemplo, também conta as tentativas recusadas. Ela não representa uma janela deslizante.
A documentação do Redis para INCR descreve a corrida que existe quando incrementar e definir a expiração são comandos independentes. Agrupá-los evita que uma interrupção entre os dois deixe um contador sem vencimento. Isso não transforma o Redis em um registro durável de consumo: persistência, failover e perda de estado ainda precisam ser considerados conforme a garantia exigida.
Se várias políticas precisam ser atendidas juntas, também é necessário definir como elas são consumidas. Uma recusa na cota por organização pode ocorrer depois de consumir a cota por usuário. Isso pode ser aceitável para contadores de tentativas. Para um orçamento que representa trabalho contratado, reservas e devoluções exigem um desenho mais cuidadoso.
Orçamentos estritos precisam de um registro de consumo
Mecanismos rápidos na borda podem escolher disponibilidade e baixa latência em troca de precisão global. Como exemplo específico, a API de rate limiting dos Cloudflare Workers aplica limites por localização e documenta consistência eventual, sem finalidade de contabilização exata. Esse contrato não deve ser generalizado para todos os limitadores nem confundido com uma cota global estrita.
Se o orçamento diário precisa ser respeitado de forma estrita, a API pode reservar o consumo em um armazenamento autoritativo antes de criar o trabalho. Em PostgreSQL, uma atualização condicional permite debitar apenas quando há saldo suficiente:
UPDATE api_budgets
SET remaining_units = remaining_units - :cost
WHERE tenant_id = :tenant_id
AND period_start = :period_start
AND remaining_units >= :cost
RETURNING remaining_units;
Esse exemplo pressupõe uma linha já provisionada e única por organização e período, custo positivo calculado no servidor e parâmetros vinculados pelo driver. Nenhuma linha retornada significa que não houve reserva, seja por falta de saldo ou de cadastro. A aplicação precisa distinguir essas situações sem iniciar a exportação por engano.
O comando, cuja forma está documentada em UPDATE no PostgreSQL, cobre apenas a redução condicional do saldo. Registrar a reserva e criar o trabalho precisam fazer parte de uma operação coordenada. Uma repetição após timeout deve reencontrar a mesma reserva, não debitar outra vez para a mesma intenção. É um uso natural de idempotência em APIs.
Se o processamento falhar, a política define o que é devolvido: tudo, parte ou nada, conforme o recurso efetivamente consumido. Reservas presas após falhas precisam de recuperação. E uma devolução exige identidade própria para não aumentar o saldo duas vezes. O rate limiter pode recusar tráfego rapidamente, enquanto esse registro mantém o compromisso de consumo.
Responda de um jeito que o cliente consiga respeitar
A RFC 6585 define 429 Too Many Requests e permite Retry-After. O corpo deve explicar a condição; a especificação não escolhe o algoritmo nem a identidade do consumidor. Uma recusa por frequência poderia ter este formato:
HTTP/1.1 429 Too Many Requests
Content-Type: application/json
Retry-After: 17
Cache-Control: no-store
{"code":"export_rate_exceeded","message":"Aguarde antes de criar outra exportação."}
O número é ilustrativo. Em uma janela, ele pode refletir o tempo restante. Num token bucket, pode vir do tempo necessário para recompor o saldo da operação, sem garantir que outras chamadas não o consumam antes. A RFC 9110 permite informar uma duração em segundos ou uma data HTTP.
Se a recusa vem de falta de capacidade global, uma resposta 503 pode representar melhor uma indisponibilidade temporária do serviço. O contrato precisa distinguir isso de cota esgotada. Não prometa um prazo exato quando a recuperação depende de um worker terminar ou de uma dependência voltar.
O cliente deve evitar repetir imediatamente e considerar o prazo total da operação. A política de retries com backoff e jitter trata dessa reação. Aqui, a responsabilidade do servidor é recusar antes de iniciar trabalho caro e informar uma condição compreensível para quem chamou.
Defina o comportamento durante uma falha do limitador
Se o estado compartilhado ficar indisponível, liberar tudo preserva disponibilidade momentânea, mas pode remover a proteção que mantém o restante do sistema funcionando. Bloquear tudo preserva a cota, mas transforma o limitador em causa de indisponibilidade. A escolha deve variar conforme a operação.
A consulta de andamento pode admitir uma proteção local de emergência, com precisão menor. Já a criação de exportações que dependem de uma reserva estrita pode retornar indisponibilidade temporária até que a reserva volte a ser possível. Essa decisão precisa ocorrer antes de o job ser criado, evitando trabalho sem contabilização.
Também vale começar com observação, registrando o que seria recusado antes de impor a política. O NGINX oferece dry run como exemplo dessa abordagem. Em qualquer implementação, compare recusas com latência, ocupação dos workers, tamanho da fila e distribuição de consumo entre clientes. A taxa de bloqueio sozinha não diz se a proteção ajudou.
Uma política útil permite explicar uma recusa: qual recurso foi protegido, quem dividia aquela cota e o que o consumidor pode fazer agora. Se essas respostas estão claras, fica mais fácil ajustar o limite sem transformar cada aumento de tráfego em um aumento automático de capacidade ou em um bloqueio generalizado.
Fontes
- OWASP API Security: consumo irrestrito de recursos
- Express: configuração de confiança em proxies
- Redis: INCR e padrões de rate limiting
- Cloudflare Workers: localidade e precisão dos limites
- PostgreSQL: UPDATE com condição e RETURNING
- RFC 6585: status 429
- RFC 9110: Retry-After
- NGINX: controle de acesso e observação dos limites

