🚦 Rate Limit¶
Un rate limiter cuenta cuántos requests llegan desde cada IP en una ventana de tiempo. Si una IP supera el límite → 429 Too Many Requests.
Sirve para:
- Cortar fuerza bruta en
/login(atacante probando passwords). - Proteger APIs públicas de scrappers / bots.
- Estabilizar el servidor ante un cliente que se desboca.
IP 1.2.3.4 100 req en 60s -> contador <= 100 -> ✅ pasa
IP 1.2.3.4 101 req en 60s -> contador > 100 -> ❌ 429 Too Many Requests
⏱ ventana se reinicia a los 60s
Algoritmo¶
HIX usa fixed window per IP - el contador se resetea de golpe al expirar la ventana:
00:00 ─────────────── 01:00 ─────────────── 02:00 ─────
IP X = 0 IP X reset IP X reset
↓ requests ↑ ↓ requests ↑ ↓ requests ↑
cuenta 1..100 vuelve a 1..100 vuelve a 1..100
| Característica | Detalle |
|---|---|
| Por qué se agrupa | IP (oReq:cIP) |
| Cuenta de qué | Cualquier request, sin distinguir ruta |
| Cuándo resetea | Al cumplirse nWindowSecs desde el primer request de esa IP |
| Penalización | 429 inmediato, sin esperar al final de la ventana |
⚠️ Limitación del fixed window: justo en el cambio de ventana, una IP puede meter
2 × nMaxrequests (al final de la ventana N y al principio de la N+1). Para tráfico normal es irrelevante; si necesitas precisión exacta, implementa sliding window aparte.
Setup¶
HIX_MwRateLimitSetup( ;
100, ; // nMax - máximo de requests
60 ) // nWindowSecs - segundos de la ventana
Defaults: 60 req / 60 s (1 req/segundo de media).
Llamar antes de oSrv:Start(). La llamada inicializa el hash compartido
y el mutex que lo protege.
Activación¶
Global - toda la app¶
Cada request entra por aquí, sea de la ruta que sea.
Por ruta¶
{ "name": "api.data", "url": "/api/data", "method": "GET",
"action": "controllers/api/data.prg",
"middleware": "HIX_MwRateLimit" }
En modo "global + por ruta", la misma cuenta se incrementa en ambos sitios. Si quieres contadores separados (por ej. global 100/min pero login 5/min) usa el factory.
Factory - límite por ruta independiente¶
HIX_MwRateLimitFactory( nMax, nWindowSecs ) devuelve un codeblock con
su propia configuración:
LOCAL bMwLogin := HIX_MwRateLimitFactory( 5, 60 ) // 5 intentos/min
oSrv:AddRoutePost( "login", "/login", bActionLogin, bMwLogin )
⚠️ Internamente todos los factories y el global comparten el hash de contadores. Lo que cambia es
nMax/nWindowSecspor llamada. Cada incremento de cualquier middleware suma al mismo contador de IP. Para contadores totalmente independientes hay que escribir un MW propio.
Comportamiento al rechazar¶
Cuando una IP supera el límite:
HIX_HttpError( oCtx:oReq, 429 ) // 429 Too Many Requests
oCtx:lHandled := .T.
RETURN .F. // corta el pipeline
El controller no llega a ejecutarse.
Leer el contador en el controller¶
HIX_MwRateLimit deja la cuenta actual en oCtx:hData["rate_count"],
útil para devolverla en cabeceras informativas:
LOCAL nCount := UContext():hData[ "rate_count" ]
USetHeader( "X-RateLimit-Remaining", hb_NToS( 100 - nCount ) )
Patrones de uso¶
Endpoint de login - protección anti-bruteforce¶
HIX_MwSessionSetup( "HIXSID", 3600, 60, "memory" )
LOCAL bMwLogin := HIX_MwRateLimitFactory( 5, 60 ) // 5 intentos/min
oSrv:AddRoutePost( "login", "/login", ;
{|| _LoginAction() }, ;
{ "HIX_MwSession", "HIX_MwCsrfCheck", bMwLogin } )
Después de 5 POSTs fallidos en 1 minuto desde la misma IP → 429 durante ~60s.
API pública¶
HIX_MwRateLimitSetup( 1000, 60 ) // 1000 req/min generosos
oSrv:Use( { "HIX_MwCors", "HIX_MwRateLimit" } )
Distintos tiers por header¶
Para clientes "premium" identificados por un header puedes saltarte el rate limit con un middleware previo:
FUNCTION MyAppSkipRateLimit( oCtx )
IF oCtx:oReq:Header( "x-premium-key", "" ) == "SECRET"
oCtx:lHandled := .T. // marca handled... no, mejor:
RETURN .T. // simplemente sigue, no aplicará el siguiente MW
ENDIF
RETURN .T.
Para tiers reales lo más limpio es un MW propio que use una clave distinta por tier en el hash de contadores.
Buenas prácticas¶
- Combina con CORS y auth. Rate limit es la primera línea, no sustituye autenticación.
- Límites estrictos en endpoints críticos.
/login,/password-reset,/2fa-verify- 5-10 intentos por minuto bastan. - Devuelve
Retry-After. Acompaña el 429 con un header que diga cuántos segundos quedan para reintentar. Útil para clientes bien programados. - Cuidado detrás de proxies. Si tu HIX está detrás de un balanceador,
oReq:cIPpuede ser la IP del proxy (todos iguales). Configura el modoproxiedpara que respeteX-Forwarded-For. - Memoria proporcional al tráfico. El hash de contadores crece con IPs únicas vistas en la ventana. Para volúmenes muy altos, añade un GC periódico que limpie entradas expiradas.
- No te juegues solo a esto. Un atacante con muchas IPs (botnet) atraviesa cualquier rate limit por IP. Combínalo con Firewall y CAPTCHA donde toque.