Cuando tu API grita "SOS": Introducción a Rate Limiting y Throttling
\n\nImagina: escribiste la aplicación Kotlin perfecta. Es rápida, elegante, usa corutinas y Flow. La publicaste en producción y, ¡bam! — un usuario "amable" (o peor, un atacante) comienza a golpear tu endpoint con miles de solicitudes por segundo. El servidor cae, la base de datos llora, y los demás usuarios reciben 503 Service Unavailable.
¿Un dolor familiar? Precisamente para esto existen los mecanismos de rate limiting (limitación de velocidad) y throttling (estrangulamiento). En este artículo no solo hablaremos de teoría, sino que analizaremos con ejemplos prácticos cómo implementar estos patrones en Kotlin.
\n\nEscribiremos código que realmente protegerá tu backend, ya sea una aplicación en Ktor, Spring Boot o corutinas puras. ¡Allá vamos!
\n\nRate Limiting vs Throttling: ¿Cuál es la diferencia (y por qué es importante)?
\n\nEmpecemos con lo básico para no confundirnos con los términos. Aunque estos conceptos se usan a menudo como sinónimos, hay un matiz entre ellos.
\n\n- \n
- Rate Limiting — es un límite estricto. "Solo puedes hacer 100 solicitudes por minuto. Después de la 101 — recibirás un
429 Too Many Requestsy bloqueo hasta el próximo minuto". Se trata de un límite. \n - Throttling — es una regulación más suave. "No puedes enviar solicitudes más rápido que 1 vez cada 200 ms. Si envías más rápido — las pondremos en cola o simplemente las ignoraremos". Se trata de la velocidad. \n
En el mundo de Kotlin, generalmente implementamos rate limiting a nivel de API (middleware) y throttling a nivel de operaciones específicas (por ejemplo, para no saturar un servidor externo con solicitudes).
\n\nAlgoritmos que todo desarrollador Kotlin debería conocer
\n\nAntes de escribir código, elijamos la herramienta. Hay tres algoritmos clásicos que encajan perfectamente con la arquitectura de Kotlin.
\n\n1. Token Bucket (Cubo de fichas) — El más popular
\nImagina un cubo al que cada segundo caen N fichas nuevas. El cubo tiene un volumen máximo (burst). Para procesar una solicitud, necesitas tomar una ficha. Si no hay fichas, la solicitud es rechazada.
\nVentajas: Permite pequeños picos de carga (bursts), es simple de implementar.
\n\n2. Leaky Bucket (Cubo con fugas) — Suavizado de picos
\nUn cubo con un agujero. Las solicitudes caen en el cubo, y el "agujero" las libera a una velocidad constante. Si el cubo se desborda, las solicitudes se descartan.
\nVentajas: Ideal para nivelar la carga, no permite picos.
\n\n3. Sliding Window (Ventana deslizante) — Precisión sin tirones
\nEn lugar de límites rígidos (como en "1 minuto"), observamos un intervalo de tiempo deslizante. Más complejo de implementar, pero refleja con mayor precisión la carga real.
\n\nPara proyectos Kotlin, Token Bucket es el punto medio dorado entre simplicidad y eficiencia. Ese es el que implementaremos.
\n\nEscribiendo código: Rate Limiter en Kotlin desde cero (sin librerías)
\n\nVamos a crear un Rate Limiter simple pero funcional, usando corutinas y Mutex. Esta será una versión thread-safe de Token Bucket.
import kotlinx.coroutines.sync.Mutex\nimport kotlinx.coroutines.sync.withLock\nimport kotlin.time.Duration\nimport kotlin.time.TimeSource\n\nclass TokenBucketRateLimiter(\n private val maxTokens: Int,\n private val refillRate: Long, // tokens por segundo\n private val timeSource: TimeSource = TimeSource.Monotonic\n) {\n private var tokens: Long = maxTokens.toLong()\n private var lastRefillTimestamp: Long = timeSource.markNow().elapsedNow().inWholeMilliseconds\n private val mutex = Mutex()\n\n suspend fun tryAcquire(): Boolean {\n mutex.withLock {\n refillTokens()\n return if (tokens > 0) {\n tokens--\n true\n } else {\n