Rate Limiting y Throttling en C#: Cómo proteger tu API de sobrecargas y mantener la calma de los usuarios
\n\nImagina: lanzaste una API increíble. El servidor vuela, la base de datos respuesta al instante. Pero de repente —¡pum!— un cliente descuidado (o, peor aún, un ataque DDoS) comienza a golpear tu punto de entrada con miles de peticiones por segundo. El servidor cae, la base de datos se asfixia y los usuarios reales ven el error 503. ¿Te suena familiar?
\n\nLa solución a este problema es el rate limiting (limitación de velocidad) y el throttling (estrangulamiento). En C# tenemos todas las herramientas para implementar estos mecanismos de forma elegante y eficiente. En este artículo no solo repasaremos la teoría, sino que escribiremos código real, analizaremos algoritmos populares y descubriremos cuál elegir para cada tarea específica. ¡Abróchate el cinturón, que se pone intenso!
\n\nRate Limiting vs Throttling: ¿Cuál es la diferencia?
\n\nAunque estos términos a menudo se confunden, existe una clara línea divisoria entre ellos:
\n- \n
- Rate Limiting es un bloqueo "duro". Si un usuario supera el límite (por ejemplo, 100 peticiones por minuto), el servidor devuelve HTTP 429 (Too Many Requests). Las peticiones simplemente no llegan a la lógica de negocio. \n
- Throttling es una ralentización "suave". El servidor acepta la petición, pero la procesa con retraso o reduce su prioridad. El usuario no recibe un error, pero su velocidad de trabajo disminuye. \n
En C# podemos implementar ambos enfoques, a menudo combinándolos para diferentes escenarios.
\n\nAlgoritmos de Rate Limiting: De lo simple a lo complejo
\n\nAntes de escribir código, analicemos las matemáticas. Existen cuatro algoritmos "dorados" que forman la base del 90% de las soluciones.
\n\n1. Fixed Window (Ventana Fija)
\nLa forma más simple. Dividimos el tiempo en intervalos fijos (por ejemplo, 1 minuto) y contamos las peticiones en cada ventana. Tan pronto como el contador alcanza el límite, bloqueamos.
\nVentajas: Simplicidad de implementación. Desventajas: Problema del "efecto borde" (ráfaga en la transición entre ventanas).
\n\npublic class FixedWindowRateLimiter\n{\n private readonly int _limit;\n private readonly TimeSpan _windowSize;\n private int _counter;\n private DateTime _windowStart;\n\n public FixedWindowRateLimiter(int limit, TimeSpan windowSize)\n {\n _limit = limit;\n _windowSize = windowSize;\n _windowStart = DateTime.UtcNow;\n }\n\n public bool AllowRequest()\n {\n var now = DateTime.UtcNow;\n if (now - _windowStart >= _windowSize)\n {\n // Reinicio de la ventana\n _counter = 0;\n _windowStart = now;\n }\n\n if (_counter >= _limit)\n return false; // Too Many Requests\n\n Interlocked.Increment(ref _counter);\n return true;\n }\n}\n\n2. Sliding Window (Ventana Deslizante)
\nEvolución de Fixed Window. En lugar de límites rígidos, consideramos las peticiones en un segmento de tiempo deslizante. Generalmente se implementa registrando marcas de tiempo (por ejemplo, en un búfer circular).
\nVentajas: Suaviza los picos. Desventajas: Un poco más complejo de implementar, requiere memoria para almacenar las marcas.
\n\npublic class SlidingWindowRateLimiter\n{\n private readonly int _limit;\n private readonly TimeSpan _windowSize;\n private readonly Queue<DateTime> _timestamps = new();\n\n public SlidingWindowRateLimiter(int limit, TimeSpan windowSize)\n {\n _limit = limit;\n _windowSize = windowSize;\n }\n\n public bool AllowRequest()\n {\n var now = DateTime.UtcNow;\n lock (_timestamps)\n {\n // Eliminar marcas obsoletas\n while (_timestamps.Count > 0 && now - _timestamps.Peek() > _windowSize)\n _timestamps.Dequeue();\n\n if (_timestamps.Count >= _limit)\n return false;\n\n _timestamps.Enqueue(now);\n return true