Quando sua API grita "SOS": Introdução ao Rate Limiting e Throttling
\n\nImagine: você escreveu uma aplicação Kotlin perfeita. Ela é rápida, elegante, usa coroutines e Flow. Você a colocou em produção e, de repente — bum! — um "bom" usuário (ou, pior, um invasor) começa a bombardear seu endpoint com milhares de requisições por segundo. O servidor cai, o banco de dados chora, e os outros usuários recebem 503 Service Unavailable.
Dor familiar? É para isso que existem os mecanismos de rate limiting (limitação de taxa) e throttling (estrangulamento). Neste artigo, não vamos apenas falar de teoria, mas analisar na prática como implementar esses padrões em Kotlin.
\n\nVamos escrever código que realmente protegerá seu backend, seja uma aplicação em Ktor, Spring Boot ou coroutines puras. Vamos lá!
\n\nRate Limiting vs Throttling: Qual a diferença (e por que é importante)?
\n\nVamos começar pela base, para não nos confundirmos com os termos. Embora esses conceitos sejam frequentemente usados como sinônimos, há uma nuance entre eles.
\n\n- \n
- Rate Limiting — é um limite rígido. "Você pode fazer apenas 100 requisições por minuto. Após a 101ª — receberá
429 Too Many Requestse será bloqueado até o próximo minuto". É sobre limite. \n - Throttling — é uma regulação mais suave. "Você não pode enviar requisições mais rápido do que 1 vez a cada 200 ms. Se enviar mais rápido — colocaremos em fila ou simplesmente ignoraremos". É sobre velocidade. \n
No mundo Kotlin, geralmente implementamos rate limiting no nível da API (middleware) e throttling no nível de operações específicas (por exemplo, para não sobrecarregar um servidor externo com requisições).
\n\nAlgoritmos que todo desenvolvedor Kotlin deve conhecer
\n\nAntes de escrever código, vamos escolher a ferramenta. Existem três algoritmos clássicos que se encaixam perfeitamente na arquitetura Kotlin.
\n\n1. Token Bucket (Balde de Tokens) — O mais popular
\nImagine um balde no qual, a cada segundo, caem N novos tokens. O balde tem um volume máximo (burst). Para processar uma requisição, é preciso pegar um token. Se não houver tokens — a requisição é rejeitada.
\nVantagens: Permite pequenos picos de carga (bursts), simples de implementar.
\n\n2. Leaky Bucket (Balde Furado) — Suavização de picos
\nUm balde com um furo. As requisições caem no balde, e o "furo" as libera a uma velocidade constante. Se o balde transbordar — as requisições são descartadas.
\nVantagens: Ideal para nivelar a carga, não permite picos.
\n\n3. Sliding Window (Janela Deslizante) — Precisão sem solavancos
\nEm vez de limites rígidos (como em "1 minuto"), olhamos para um intervalo de tempo deslizante. Mais complexo de implementar, mas reflete com mais precisão a carga real.
\n\nPara projetos Kotlin, o Token Bucket é o meio-termo ideal entre simplicidade e eficiência. É ele que vamos implementar.
\n\nEscrevendo o código: Rate Limiter em Kotlin do zero (sem bibliotecas)
\n\nVamos criar um Rate Limiter simples, mas funcional, usando coroutines e Mutex. Esta será uma versão thread-safe do 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