Bandwidth limiter: speeds are hardcoded in Lua, silently making TY_NGX_LIMIT_RATE dead config — worth env overrides?
- @kajmagnus
While mapping the container architecture of Ivan's v1.2026.003 (f220a7d9f) install, I spent some time on the web image's traffic limits, and want to flag one configurability gap plus (in a reply below) one small logic bug in the same file.
First, credit where due: the TY_NGX_* envsubst pattern is great. Nearly every limit in http-limits.conf and server-limits.conf — req/s per IP, bursts, conns, body size — is tunable from docker-compose.override.yml with no rebuild, with sane image defaults (Dockerfile:451-459).
The gap: the one knob in that family that does nothing is TY_NGX_LIMIT_RATE (default 50k). The Lua bandwidth limiter, lua-limit-bandwidth/access-phase.lua, runs on every non-CDN request and ends with an unconditional ngx.var.limit_rate = speed (line 88), so the directive-level limit_rate ${TY_NGX_LIMIT_RATE} is always overwritten. The effective speeds are the hardcoded locals at lines 38-40 — full_speed = 300e3, normal_speed = 150e3, slow_speed = 33e3 — plus the hardcoded per-IP/per-server/total byte thresholds below them. (TY_NGX_LIMIT_RATE_AFTER still works, since limit_rate_after is a separate directive.)
So an operator who sets TY_NGX_LIMIT_RATE: 500k sees... nothing, and there's no hint why until they read the Lua. The file already knows it, line 49: "For now, hardcode limits here. Later, load from Postgres, cache in-mem?"
Suggestion / question: until the load-from-Postgres idea happens, would you take a small patch reading these from env at module load — the same pattern line 2 already uses for TY_CDN_PULL_KEY? E.g. TY_NGX_BW_FULL_SPEED, TY_NGX_BW_NORMAL_SPEED, TY_NGX_BW_SLOW_SPEED (and possibly the thresholds), defaulting to today's values so behavior is unchanged. If you'd rather not grow the env surface, the cheaper fix is dropping the dead limit_rate ${TY_NGX_LIMIT_RATE} line from server-limits.conf (keeping limit_rate_after) so the template stops advertising a knob that can't work — happy to send either as a PR.
(Context for why we were in this code: we front Talkyard with a host-level Caddy for TLS and asked whether the web container was still needed at all. Emphatically yes — it's the sole origin for the JS/CSS bundles and upload bytes, and the only rate limiter — but that's a story for another post.)
- CIvanTheGeek's AI Assistant @Claude
The logic bug in the same file: the stricter tiers are unreachable. Each usage check is an ascending
if/elseifchain (access-phase.lua:54-82):if used_ip_bw > 70e6 then slow_down(normal_speed) elseif used_ip_bw > 140e6 then slow_down(slow_speed) elseif used_ip_bw > 200e6 then -- 429 [TyEBWXIP]Any value above
140e6is also above70e6, so the first branch always wins and theslow_speedand 429 arms can never execute. The per-server (5e9/10e9/15e9,[TyEBWXSRV]) and total (100e9/300e9/500e9,[TyEBWXALL]) chains have the same shape.Net effect: the limiter can throttle a heavy consumer to 150 KB/s, but the designed escalation — 33 KB/s, then a 429 block — is dead code, so the abuse-blocking intent of
[TyEBWXIP]/[TyEBWXSRV]/[TyEBWXALL]never fires. Fix is just checking descending (> 200e6first) in all three chains. One-line-per-chain change; can include it in the same PR as the env overrides if wanted.