Capítulo 05 · El portero

Rate limiting

Cuántas veces podés llamar a la puerta. Login, 429, token bucket. Redis otra vez, otro job.

11 min intermedio

Alguien golpea el login

Un script prueba contraseñas contra POST /login. Diez mil intentos por minuto, todos desde la misma IP, todos sobre la cuenta de Ana. Sin límite, Postgres y bcrypt trabajan para el atacante. El checkout no es inmune: un bot refresca Comprar, o un cliente mal programado reintenta el pago en un bucle. Stripe también te ratea a vos; si no te rateás antes, su 429 se convierte en tu incidente.

Rate limiting es decir: esta clave, en esta ventana, como máximo N veces. Después, 429 Too Many Requests, a menudo con Retry-After. No es un lock. El lock del 42 dice «este par es de Ana diez minutos». El límite dice «Ana (o esta IP, o esta API key) ya llamó bastante». Tampoco es el backpressure de las colas: allí frenabas porque los workers no daban abasto. Acá frenás antes de que el trabajo nazca, aunque el cluster esté holgado. Protegés abuso, coste y a los vecinos.

Qué cuentas, y de quién

La pregunta de diseño no es el algoritmo. Es la clave.

Por IP en el login: el script se nota. El precio es un edificio o un móvil detrás de CGNAT: mucha gente, una IP, un 429 injusto. Por user id cuando ya hay sesión: el checkout de Ana no tumba el de Luis. Por API key si vendés una API a terceros: el plan gratis son 100 req/min. A veces combinás: login duro por IP y por email.

El 123 viralizado no se arregla rateando GET de ficha por IP a 10/min: echás a clientes reales. Ahí el colchón era caché. El rate limit en lecturas públicas, si existe, es alto y sirve contra scrapers, no contra Black Friday. En writes sí apretás: login, reset de password, Comprar, «enviar email de factura».

Dónde lo ponés: en el API gateway si es un número gordo y uniforme. En la aplicación si la regla es de negocio («3 intentos de pago por pedido»). Un gateway que no sabe qué es un pedido no puede limitar «por order_id».

Ventanas que mienten, cubos que gotean

El contador naïf: «en este minuto, 60 requests». A las 12:00:59 disparás 60, a las 12:01:00 otras 60. En dos segundos hiciste 120. Eso es fixed window. Simple, un INCR con TTL de 60s. El borde de la ventana es el truco. Vale para un primer corte. No vale si alguien puede abusar del borde.

Sliding window mira los últimos 60 segundos de verdad. Más justo, un poco más de trabajo.

Token bucket es el que mejor se cuenta. Un cubo con capacidad 10. Cada segundo entra 1 token. Cada request gasta uno. Si el cubo está lleno, Ana puede pegar un burst de 10 (abrió la app y cargó diez cosas) y después se suaviza. Si está vacío, 429. Encaja con humanos: un pico corto sí, un fuego constante no. Leaky bucket es el inverso: salen a ritmo fijo; el resto espera o se tira.

No memorices cuatro nombres. Di: I want a burst of a few logins and then a hard cap; token bucket in Redis, keyed by IP for /login.

Redis, otra vez, otro job

Tres APIs detrás del load balancer. Si cada una cuenta en memoria, Ana hace 60 en cada una: 180. El límite tiene que ser compartido. Redis, el de la ficha y el del lock, ahora es un contador:

INCR ratelimit:login:ip:1.2.3.4
EXPIRE ... 60    -- solo si acabás de crear la key

Si el valor pasa de N, 429. El EXPIRE hay que ponerlo cuando la key es nueva; si no, un INCR eterno no caduca nunca. En la pizarra: atomic incr, TTL = window, compare to N.

Token bucket en Redis: tokens y timestamp de la última recarga, recalculás al vuelo. Cuidado con la hot key: una IP de un botnet martillea ratelimit:ip:.... El 429 es barato (no vas a bcrypt). Si hace falta, un límite tosco en el gateway por IP antes del fino en app.

¿Redis cae? Fail closed: sin contador, nadie pasa. La tienda se siente caída. Fail open: sin contador, todos pasan. La tienda vive, el script también. El login suele fail closed o con un límite local de emergencia. El GET de ficha, fail open. En entrevista elegí y di por qué.

429 no es el final

El cliente decente espera Retry-After y hace backoff — el mismo jitter de las colas. El bot no. El límite no sustituye captcha, WAF ni bloquear IPs sucias; se lleva bien con ellos. En APIs de partners, el 429 es contrato. En el checkout, un 429 a un humano pide un mensaje claro, no un JSON crudo.

A veces no tirás la request: la encolás. «Te mandamos el PDF cuando toque». Rate limit y cola se dan la mano: el límite protege la entrada síncrona; la cola alisa lo que puede esperar. No metas en cola el login. Sí el «enviar factura otra vez».

Stripe, SMS, email: ellos te limitan a vos. Un rate limiter de salida evita que tus workers, en un retry storm, agraven su 429.

La tienda, con portero

Login: 5 intentos / 15 min por IP y por email. Token bucket o ventana deslizante. Fail relativamente cerrado. Checkout: unas pocas compras por user por minuto, no por IP (una oficina no debe bloquearse). Email de factura: 3 al día por pedido. GET de ficha: límite alto o ninguno, caché delante. Workers hacia Stripe: cubo global para no quemar su cuota.

Todo en Redis, keys con TTL, 429 con Retry-After. El lock del 42 sigue aparte: Luis no reserva el par; eso no es un 429, es «esta talla está apartada».

Qué espera cada nivel

Un mid pone un 429, Redis INCR y una ventana.

Un senior elige la clave (IP vs user vs key), distingue lecturas cacheadas de writes, y dice fail open o closed según la ruta.

Si te lo preguntan

I’d rate-limit login by IP and by email, token bucket in Redis so all API instances share the count. Over the limit we return 429 with Retry-After. Product reads are cached, not squeezed — Black Friday is not an attack. If Redis is down, login fails closed or uses a tight local cap; catalog reads fail open.

¿Fixed o sliding? Fixed is simple and bursts at the window edge. I’d use sliding or a token bucket if fairness matters.

¿Por qué Redis? The limit has to be global across instances. Memory on one box is not a limit, it’s a suggestion.

¿En el gateway? Yes for coarse caps. Business rules — per order, per user plan — live in the app.