Limitación de Tasa y Throttling en Go: cómo proteger tu API y no volverte loco
\n\nImagina: has escrito una API perfecta en Go. Vuela, procesa solicitudes en milisegundos, y de repente — ¡pum! — llega una avalancha de solicitudes al servidor. Puede ser un bot, un error en el cliente, o simplemente el hype alrededor de tu producto. Sin protección, el servidor caerá, los usuarios se irán, y tú estarás revisando los registros hasta la mañana.
\n\n¿Te suena familiar? Entonces rate limiting (limitación de velocidad) y throttling (estrangulamiento) son tus nuevos mejores amigos. En este artículo en pythonlib.ru veremos cómo implementarlos en Go, un lenguaje creado para sistemas de alta carga. Sin rodeos, solo código real y casos prácticos.
\n\nRate Limiting vs Throttling: ¿cuál es la diferencia?
\n\nPara empezar, aclaremos los términos. Muchos confunden estos conceptos, pero hay una diferencia.
\n\n- \n
- Rate Limiting — es una restricción estricta: "No más de 100 solicitudes por minuto". Si se excede el límite, el servidor responde con 429 Too Many Requests. Todo claro y estricto. \n
- Throttling — es una regulación suave: "Te ralentizaremos, pero no te bloquearemos". Por ejemplo, en lugar de responder inmediatamente a todas las solicitudes, las ponemos en una cola y las procesamos a una velocidad fija. \n
En Go, ambos enfoques se implementan de manera elegante gracias a las gorutinas y los canales. Veamos ejemplos concretos.
\n\nLimitador de tasa simple con canales
\n\nLa forma más básica de limitar la velocidad es usar un ticker del paquete time. Es como un metrónomo: cada "tick" permite una solicitud.
package main\n\nimport (\n "fmt"\n "time"\n)\n\nfunc main() {\n // Creamos un canal que enviará tokens cada 200 ms\n limiter := time.Tick(200 * time.Millisecond)\n\n // Simulamos 10 solicitudes\n for i := 1; i <= 10; i++ {\n <-limiter // Esperamos permiso\n fmt.Printf("Solicitud %d procesada a las %s\\n", i, time.Now().Format("15:04:05.000"))\n }\n}\n\n¿Qué sucede aquí? time.Tick crea un canal que envía un valor cada 200 milisegundos. Nuestra gorutina se bloquea en <-limiter hasta que recibe un token. El resultado: exactamente 5 solicitudes por segundo, sin variaciones.
Inconveniente: si hay menos solicitudes que ticks, simplemente esperamos. ¿Ineficiente? Sí. Pero para escenarios simples es ideal.
\n\nLímite con ráfagas (Burst) usando un búfer
\n\n¿Y si necesitamos permitir "ráfagas" de tráfico? Por ejemplo, permitir 10 solicitudes instantáneamente y luego limitar a 5 por segundo. Para esto usamos un canal con búfer: el clásico Token Bucket.
\n\npackage main\n\nimport (\n "fmt"\n "time"\n)\n\nfunc main() {\n // Canal con búfer para 5 tokens\n tokens := make(chan struct{}, 5)\n\n // Gorutina que llena: añade un token cada 200 ms\n go func() {\n ticker := time.NewTicker(200 * time.Millisecond)\n defer ticker.Stop()\n for range ticker.C {\n select {\n case tokens <- struct{}{}:\n default:\n // El canal está lleno — no hacemos nada\n }\n }\n }()\n\n // Simulación de 15 solicitudes\n for i := 1; i <= 15; i++ {\n <-tokens // Tomamos un token (esperamos si está vacío)\n fmt.Printf("Solicitud %d procesada a las %s\\n", i, time.Now().Format("15:04:05.000"))\n time.Sleep(50 * time.Millisecond) // Pequeña pausa entre solicitudes\n }\n}\n\nLas primeras 5 solicitudes pasarán instantáneamente (el búfer está lleno), y luego una cada 200 ms. Este patrón es ideal para APIs donde el cliente puede empezar a enviar muchas solicitudes de repente (por ejemplo, después de un reinicio).
\n\nThrottling con WaitGroup y Worker Pool
\n\nAhora pasemos al throttling. Supongamos que tenemos una cola de tareas (por ejemplo, envío de correos masivos). No queremos bloquear al cliente, pero tampoco sobrecargar