Throttling
Um atendente no topo de um tobogã aquático. Ele não manda as crianças embora, ele faz cada uma esperar um pouco mais que a anterior.
Throttling deixa as requisições mais lentas depois que uma certa quantidade já passou, em vez de recusá-las ao atingir um limite rígido. O cliente ainda recebe uma resposta, só que mais tarde.
Um atraso em vez de uma recusa
express-slow-down é um middleware, com o mesmo formato do limiter: ele conta as requisições em uma janela de tempo e começa a atrasá-las depois de ultrapassar um limite.
import { slowDown } from 'express-slow-down'
const throttle = slowDown({
windowMs: 10000,
delayAfter: 2,
delayMs: () => 5000,
})
app.get('/api/data', throttle, handler)Três configurações: a janela, quantas passam sem serem afetadas, e por quanto tempo tudo é atrasado depois disso.
Dispare cinco requisições de uma vez e as duas primeiras retornam imediatamente. As outras três esperam cinco segundos cada, e então são bem-sucedidas.
| Requisição | Chegou | Atraso | Respondeu |
|---|---|---|---|
| 1 | 0s | nenhum | 0s |
| 2 | 0s | nenhum | 0s |
| 3 | 0s | 5s | 5s |
| 4 | 0s | 5s | 5s |
| 5 | 0s | 5s | 5s |
Nada foi recusado. Todas as requisições foram bem-sucedidas, e três delas demoraram cinco segundos a mais do que gostariam.
É esse o atrativo e também a armadilha. Nada quebra, mas nada avisa o cliente para diminuir o ritmo também.
Com que velocidade o atraso cresce
delayMs é uma função de hits, o número de requisições contadas na janela, então o atraso pode crescer junto com a pressão.
delayMs: () => 5000 // fixo: sempre 5s
delayMs: (hits) => (hits - 2) * 1000 // incremental: 1s, 2s, 3s
delayMs: (hits) => hits * hits * 1000 // exponencial: 9s, 16s, 25sCom delayAfter: 2, os três formatos se comportam de maneira bem diferente na quinta requisição:
| Hit | Fixo | Incremental | Exponencial |
|---|---|---|---|
| 3 | 5s | 1s | 9s |
| 4 | 5s | 2s | 16s |
| 5 | 5s | 3s | 25s |
O incremental subtrai delayAfter, então a primeira requisição atrasada espera um segundo em vez de três. O exponencial eleva a contagem de hits ao quadrado, então ele sobe de forma tão acentuada que uma pressão sustentada rapidamente se torna dolorosa.
A curva que você escolhe importa mais do que o limite em si, e isso só fica claro quando você compara os números lado a lado.
O que a janela conta
express-slow-down usa uma janela deslizante. Cada requisição olha para trás windowMs e conta os hits dentro desse período, e se nada chegou por mais tempo do que isso, a contagem recomeça do zero.
O detalhe sutil é o que conta como estar dentro da janela: as requisições são contadas pelo momento em que chegaram, não pelo momento em que foram processadas.
Com windowMs: 10000 e delayAfter: 2, cinco requisições chegando de uma vez e mais cinco aos 30 segundos:
- As cinco primeiras chegam todas em 0s. Duas passam, três são atrasadas e respondem depois.
- A segunda rajada chega em 30s. Olhando dez segundos para trás cobre de 20s a 30s, e nada chegou nesse intervalo, mesmo que respostas atrasadas da primeira rajada ainda estivessem sendo enviadas.
- Então a contagem recomeça do zero, duas passam, e três são atrasadas de novo.
Respostas atrasadas saindo do servidor não estendem a janela. Só chegadas novas fazem isso.
Aquelas crianças esperando já foram contadas quando chegaram. Contá-las de novo seria cobrar duas vezes pela mesma visita.
Combinando um throttle e um limiter
Os dois se combinam, e a ordem importa porque o Express executa os middlewares da esquerda para a direita:
app.get('/api/data', throttle, limiter, handler)Colocar o throttle primeiro significa que as requisições vão ficando progressivamente mais lentas, e só depois de um teto rígido é que o limiter as recusa. Inverta a ordem e o limiter corta os clientes antes que o throttle chegue a ser gentil com eles.
Um briefing prático do curso. O sistema se degrada depois de três requisições em dez segundos, então atrase a quarta em 1s, a quinta em 4s, a sexta em 9s. E não mais que sete requisições em trinta segundos, no total.
import { slowDown } from 'express-slow-down'
import { rateLimit } from 'express-rate-limit'
const throttle = slowDown({
windowMs: 10000,
delayAfter: 3,
delayMs: (hits) => (hits - 3) * (hits - 3) * 1000,
})
const limiter = rateLimit({
limit: 7,
windowMs: 30000,
message: 'Too many requests',
standardHeaders: 'draft-6',
legacyHeaders: false,
})A fórmula do atraso é montada de trás para frente, a partir do padrão exigido. O hit 4 precisa dar 1, o hit 5 precisa dar 4, o hit 6 precisa dar 9, que são 1², 2², 3². Subtrair delayAfter transforma o número do hit na base, e elevar ao quadrado produz a curva.
Execute sete requisições rápidas e algo inesperado acontece. A requisição 7, atrasada em dezesseis segundos, chega ao limiter depois da requisição 8, que teve um cronômetro mais curto. A requisição 8 se torna o sétimo sucesso e a requisição 7 é recusada.
Requisições throttled não formam uma fila. Cada uma tem seu próprio cronômetro e chega quando chega.
Um leaky bucket as manteria em fila. Um throttle distribui cronômetros e os deixa correr.
Coloque em prática
Um endpoint está configurado com windowMs: 10000, delayAfter: 2, delayMs: (hits) => hits * hits * 1000, e nenhum rate limiter.
- Quatro requisições chegam ao mesmo tempo. Quando cada uma responde?
- Um cliente envia quatro requisições a cada 15 segundos, para sempre. Os atrasos pioram com o tempo?
- O que acontece se uma quinta requisição chega e seu atraso ultrapassa o timeout do cliente?
- Como você mudaria a configuração para que um único cliente não conseguisse deixar todo mundo mais lento?
Compare suas respostas
| # | Resposta |
|---|---|
| 1 | As requisições 1 e 2 respondem imediatamente. A requisição 3 é o hit 3, então 3² = 9 segundos. A requisição 4 é o hit 4, então 4² = 16 segundos |
| 2 | Não. A janela é de 10 segundos e o intervalo é de 15, então cada rajada encontra uma janela vazia e a contagem recomeça do zero. O mesmo padrão de duas livres, depois 9s, depois 16s se repete para sempre |
| 3 | O cliente desiste e seu servidor continua segurando a conexão até o atraso expirar. O trabalho é feito e ninguém o recebe, e é por isso que atrasos exponenciais precisam de maxDelayMs |
| 4 | Adicione um keyGenerator para que o throttle conte por cliente em vez de globalmente. Sem isso, cada requisição compartilha um único orçamento e um cliente pesado atrasa todo mundo |
A pergunta 2 é a que costuma surpreender as pessoas. Um atraso exponencial parece que deveria punir o abuso sustentado, mas um cliente que espaça suas rajadas para ficar fora da janela nunca sente esse crescimento. A curva só morde dentro de uma única janela.
Para onde isso leva
Cinco algoritmos e dois controles, todos respondendo a uma única pergunta: quanto esse cliente pode pedir, e o que acontece quando ele pede mais que isso.
O raciocínio vale além dos seus próprios servidores. Toda API externa que você chama tem seus próprios limites, então entender isso melhora também o seu código de cliente: como ele recua, e como ele lida com um 429 em vez de continuar martelando um serviço que já está sobrecarregado.
Com isso, encerramos o handbook. Cinco seções, desde pensar como um atacante até segurança de entrada, identidade e prevenção de abuso, cada uma construída sobre um exemplo funcional que você pode quebrar e depois consertar.

