Throttling
Un encargado en la punta de un tobogán acuático. No rechaza a los niños, hace que cada uno espere un poco más que el anterior.
El throttling ralentiza las solicitudes después de que pasa cierta cantidad, en lugar de rechazarlas al llegar a un tope fijo. El cliente igual recibe una respuesta, solo que más tarde.
Una demora en lugar de un rechazo
express-slow-down es un middleware con la misma lógica que el limiter: cuenta las solicitudes dentro de una ventana de tiempo y empieza a demorarlas después de cierto umbral.
import { slowDown } from 'express-slow-down'
const throttle = slowDown({
windowMs: 10000,
delayAfter: 2,
delayMs: () => 5000,
})
app.get('/api/data', throttle, handler)Tres parámetros: la ventana de tiempo, cuántas solicitudes pasan sin problema, y cuánto se demora todo lo que viene después.
Dispara cinco solicitudes al mismo tiempo y las dos primeras responden de inmediato. Las otras tres esperan cinco segundos cada una y luego se completan con éxito.
| Solicitud | Llegó | Demora | Respondió |
|---|---|---|---|
| 1 | 0s | ninguna | 0s |
| 2 | 0s | ninguna | 0s |
| 3 | 0s | 5s | 5s |
| 4 | 0s | 5s | 5s |
| 5 | 0s | 5s | 5s |
Ninguna fue rechazada. Todas las solicitudes tuvieron éxito, y tres de ellas tardaron cinco segundos más de lo que hubieran querido.
Ahí está el atractivo y también la trampa. Nada se rompe, pero tampoco hay nada que le indique al cliente que baje el ritmo.
Qué tan rápido crece la demora
delayMs es una función de hits, la cantidad de solicitudes contadas en la ventana, así que la demora puede crecer con la presión.
delayMs: () => 5000 // fija: siempre 5s
delayMs: (hits) => (hits - 2) * 1000 // incremental: 1s, 2s, 3s
delayMs: (hits) => hits * hits * 1000 // exponencial: 9s, 16s, 25sCon delayAfter: 2, las tres formas se comportan muy distinto para la quinta solicitud:
| Hit | Fija | Incremental | Exponencial |
|---|---|---|---|
| 3 | 5s | 1s | 9s |
| 4 | 5s | 2s | 16s |
| 5 | 5s | 3s | 25s |
La incremental resta delayAfter, así que la primera solicitud demorada espera un segundo en lugar de tres. La exponencial eleva al cuadrado el número de hit, así que sube tan rápido que la presión sostenida se vuelve dolorosa muy pronto.
La curva que elijas importa más que el umbral, y eso solo se hace evidente cuando ves los números uno al lado del otro.
Qué cuenta la ventana
express-slow-down usa una ventana deslizante. Cada solicitud mira hacia atrás windowMs y cuenta los hits dentro de ese rango, y si no llegó nada en un período más largo que ese, el conteo vuelve a empezar de cero.
El detalle sutil es qué cuenta como estar dentro de la ventana: las solicitudes se cuentan según cuándo llegaron, no según cuándo se procesaron.
Con windowMs: 10000 y delayAfter: 2, cinco solicitudes que llegan a la vez y otras cinco a los 30 segundos:
- Las primeras cinco llegan todas a los 0s. Dos pasan, tres se demoran y responden más tarde.
- La segunda ráfaga llega a los 30s. Mirar hacia atrás diez segundos cubre de los 20s a los 30s, y no llegó nada en ese tramo, aunque las respuestas demoradas de la primera ráfaga todavía se estaban enviando.
- Así que el conteo empieza de cero otra vez, dos pasan, y tres se demoran de nuevo.
Las respuestas demoradas que salen del servidor no extienden la ventana. Solo las nuevas llegadas lo hacen.
Esos niños que están esperando fueron contados cuando llegaron. Contarlos dos veces sería cobrarles por la misma visita.
Combinar un throttle y un limiter
Los dos se pueden combinar, y el orden importa porque Express ejecuta los middlewares de izquierda a derecha:
app.get('/api/data', throttle, limiter, handler)Poner el throttle primero significa que las solicitudes se ralentizan progresivamente, y solo pasado un tope fijo el limiter las rechaza. Invierte el orden y el limiter corta a los clientes antes de que el throttle llegue siquiera a ser suave con ellos.
Un caso práctico del curso. El sistema se degrada después de tres solicitudes en diez segundos, así que la cuarta debe demorarse 1s, la quinta 4s, la sexta 9s. Y no más de siete solicitudes en treinta segundos, en 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,
})La fórmula de la demora se construye hacia atrás a partir del patrón requerido. El hit 4 debe dar 1, el hit 5 debe dar 4, el hit 6 debe dar 9, que son 1², 2², 3². Restar delayAfter convierte el número de hit en la base, y luego elevarlo al cuadrado genera la curva.
Ejecuta siete solicitudes rápidas seguidas y pasa algo inesperado. La solicitud 7, demorada dieciséis segundos, llega al limiter después que la solicitud 8, que tuvo un temporizador más corto. La solicitud 8 se convierte en el séptimo éxito y la solicitud 7 es rechazada.
Las solicitudes con throttle no forman una cola. Cada una tiene su propio temporizador y llega cuando llega.
Un leaky bucket las hubiera mantenido en orden. Un throttle reparte temporizadores y los deja correr.
Ponlo a prueba
Un endpoint está configurado con windowMs: 10000, delayAfter: 2, delayMs: (hits) => hits * hits * 1000, y sin ningún rate limiter.
- Llegan cuatro solicitudes a la vez. ¿Cuándo responde cada una?
- Un cliente envía cuatro solicitudes cada 15 segundos, indefinidamente. ¿Las demoras empeoran con el tiempo?
- ¿Qué pasa si llega una quinta solicitud y su demora supera el timeout del cliente?
- ¿Cómo cambiarías la configuración para que un solo cliente no pueda ralentizar a todos los demás?
Compara tus respuestas
| # | Respuesta |
|---|---|
| 1 | Las solicitudes 1 y 2 responden de inmediato. La solicitud 3 es el hit 3, así que 3² = 9 segundos. La solicitud 4 es el hit 4, así que 4² = 16 segundos |
| 2 | No. La ventana es de 10 segundos y el intervalo es de 15, así que cada ráfaga mira hacia atrás a una ventana vacía y empieza a contar desde cero. El mismo patrón de dos gratis, luego 9s, luego 16s se repite indefinidamente |
| 3 | El cliente se rinde y tu servidor sigue reteniendo la conexión hasta que expira la demora. El trabajo se hace y nadie lo recibe, por eso las demoras exponenciales necesitan maxDelayMs |
| 4 | Agrega un keyGenerator para que el throttle cuente por cliente en lugar de globalmente. Sin eso, cada solicitud comparte un único presupuesto y un cliente intensivo ralentiza a todos |
La pregunta 2 es la que sorprende a la gente. Una demora exponencial suena como si castigara el abuso sostenido, pero un cliente que espacia sus ráfagas para caer fuera de la ventana nunca la siente subir en absoluto. La curva solo muerde dentro de una misma ventana.
Hacia dónde va esto
Cinco algoritmos y dos controles, todos respondiendo una sola pregunta: cuánto se le permite pedir a este cliente, y qué pasa cuando pide más.
El razonamiento se extiende más allá de tus propios servidores. Cada API externa que consumes tiene sus propios límites, así que entender esto también mejora tu código del lado del cliente: cómo baja el ritmo y cómo maneja un 429 en lugar de seguir insistiendo contra un servicio que ya está sufriendo.
Con esto se cierra el manual. Cinco secciones, desde pensar como un atacante hasta la seguridad de entradas, la identidad y la prevención de abuso, cada una construida sobre un ejemplo funcional que puedes romper y luego arreglar.

