From 4a1211a93152cd64f502a02dfb451db97aa2072c Mon Sep 17 00:00:00 2001 From: openhands Date: Thu, 1 Oct 2026 17:25:49 +0200 Subject: [PATCH] feat(proxy): add per-IP rate and connection limits The edge had no limit_req/limit_conn at all, so a single client could flood the Next.js backend and the Nitro client with unbounded parallel requests. Traefik's logs already showed this: bursts of gamedata icon requests answered with 429. Add limit_req (30r/s, burst 60, nodelay) and limit_conn (30) zones keyed on the real client IP, applied at server scope so both cached assets and proxied API routes share one budget. The burst is deliberately generous because the Nitro client fetches gamedata and icons in bursts when loading a room. --- deployment/proxy/nginx-cms.conf | 7 +++++++ deployment/proxy/nginx.conf | 6 ++++++ 2 files changed, 13 insertions(+) diff --git a/deployment/proxy/nginx-cms.conf b/deployment/proxy/nginx-cms.conf index 04725b77..e21659da 100644 --- a/deployment/proxy/nginx-cms.conf +++ b/deployment/proxy/nginx-cms.conf @@ -192,6 +192,13 @@ server { keepalive_timeout 30s; send_timeout 10s; + # Abuse limits. Applied per server, not per location, so cached assets and + # proxied API routes are all covered by the same budget. nodelay keeps the + # 60-request burst responsive: allowed requests pass immediately, only the + # excess is rejected with 503 instead of being queued. + limit_req zone=cms_req_per_ip burst=60 nodelay; + limit_conn cms_conn_per_ip 30; + # Traefik health-check route herstellen location = /health { access_log off; diff --git a/deployment/proxy/nginx.conf b/deployment/proxy/nginx.conf index 980ccae0..dae1800f 100644 --- a/deployment/proxy/nginx.conf +++ b/deployment/proxy/nginx.conf @@ -44,6 +44,12 @@ http { client_header_buffer_size 1k; large_client_header_buffers 4 8k; + # Rate limiting per client IP. The Nitro client fetches gamedata and icons in + # bursts when booting a room, so the burst is deliberately generous: it caps + # sustained floods without punishing a normal room load. + limit_req_zone $binary_remote_addr zone=cms_req_per_ip:10m rate=30r/s; + limit_conn_zone $binary_remote_addr zone=cms_conn_per_ip:10m; + # Blue/green cutover: ci-deploy.sh writes the active upstream here, and # `proxy_pass http://cms_app` below follows it via graceful nginx -s reload. upstream cms_app {