yum upgrades for production use, this is the repository for you.
Active subscription is required.
Technical Briefing: Sub-Hourly NGINX Rate Limits with NGINX-MOD
The Problem
Stock NGINX limit_req_zone accepts only two rate units: r/s and r/m. A directive like rate=10r/h fails nginx -t with invalid rate, because the parser matches only those two suffixes and then attempts ngx_atoi on the remainder.
There is a second, quieter defect. Internally, upstream stores the rate as milli-requests-per-second:
ctx->rate = rate * 1000 / scale
This silently truncates rates slower than roughly 4r/h to zero, so the leaky bucket never refills at hourly-and-slower cadences. The configuration may pass validation while enforcing nothing meaningful.
The Fix
NGINX-MOD adds five rate units to limit_req_zone: r/h, r/d, r/w, r/M (month), and r/Y (year), backed by high-resolution bucket storage that expresses rates down to 1r/Y without precision loss.
Unit Semantics and the r/M Trap
Month and year units use astronomical averages, not calendar months or leap-aware years. For example, 1000r/M means 1000 per 2,592,000 seconds.
r/M is uppercase because lowercase r/m already means minutes. Mistyping 100r/m for 100r/M silently yields a per-minute limit roughly 43,200x stricter, with no warning.
Installation
NGINX-MOD is a drop-in replacement for stock nginx; existing r/s and r/m configs behave identically.
- RPM: enable the
getpagespeed-extras-nginx-modsubchannel (shipped disabled by default), thendnf swap nginx nginx-mod. - DEB: add the APT repo, then
apt-get install nginx-mod.
Verification
Verified on Rocky Linux 10: after dnf config-manager --set-enabled getpagespeed-extras-nginx-mod and dnf swap nginx nginx-mod, the same config that previously failed now passes nginx -t. Ten parallel curls against a rate=10r/h burst=2 nodelay static-file location yield exactly 3×200 and 7×429, with rejections logged as “limiting requests” in the error log.
Confirm the active binary with nginx -v (expect the nginx-mod by GetPageSpeed.com/ prefix) or rpm -q nginx-mod. A plain nginx/X.Y.Z string means stock, and the new units won’t parse.
Production Recipes
rate=1000r/d burst=20 nodelay— free-tier daily ceilingsrate=10r/h burst=3 nodelay— dedicated password-reset locationrate=5000r/w burst=50 nodelaykeyed on$http_authorization(a 20m zone holds ~320,000 keys)rate=10000r/M burst=200 nodelay— webhooks (~one per 4.3 minutes average)
Caveats
Zone Sizing Is the Key Operational Risk
limit_req stores only a leaky-bucket excess and millisecond timestamp per key in an rb-tree node. A 10m zone holds ~160,000 IPv4 entries; 1m holds ~16,000.
If unique keys in the window exceed zone capacity, ngx_http_limit_req_expire force-evicts oldest entries, giving returning visitors excess=0 and a fresh burst allowance — defeating the quota. Size for unique keys expected within the rate window, not concurrent users.
Testing Gotcha
return 200 runs in NGX_HTTP_REWRITE_PHASE, before limit_req‘s NGX_HTTP_PREACCESS_PHASE, so it short-circuits the limiter and every request returns 200. Test through a content-phase handler instead — static file with root, try_files, proxy_pass, or fastcgi_pass.
Scope
limit_req is single-node: buckets are per-worker/per-server. Fleet-wide quotas require the NGINX Redis rate limit module; application-layer billing tiers belong in the app or an API gateway.
The Payoff
Sub-hourly and sub-daily rate limits — daily free-tier ceilings, hourly password-reset caps, weekly authorization-key quotas, monthly webhook budgets — become expressible directly in NGINX configuration, without an external rate-limiting layer, and without the silent truncation that makes stock NGINX’s slowest rates unenforceable.
Read the full article: NGINX limit_req Per Hour, Day, Week, Month with NGINX-MOD
