Limit the rate per API key
nginx’s limit_req counts requests per key in a shared memory zone, which
all workers use, and rejects the requests over the rate. The key is any
nginx variable. A Ruby script computes it with
mruby_set, so
that the limit applies to a value that Ruby derives from the request, such
as an API key, while nginx keeps the counters.
The sample
Section titled “The sample”The script returns the API key of the header X-API-Key, or the client
address for a request without one, so that no request has an empty key:
limit_req does not count a request whose key is empty. The zone allows
one request per minute and key. limit_req_status 429 and
error_page 429 @limited give the rejected request the status 429 and a
JSON body.
limit_req_zone $api_key zone=api_keys:1m rate=1r/m;
server { listen 18166;
location /api/ { mruby_set $api_key build/nginx/html/guide_rate_limit.rb; limit_req zone=api_keys; limit_req_status 429; error_page 429 @limited; mruby_content_handler_code 'Nginx.rputs "accepted\n"'; }
location @limited { default_type application/json; return 429 '{"error":"rate_limited"}\n'; }}r = Nginx::Request.newkey = r.headers_in["X-API-Key"].to_skey.empty? ? "addr:#{r.var.remote_addr}" : "key:#{key}"GET /api/orders HTTP/1.1Host: 127.0.0.1:18166X-API-Key: demo-keyConnection: closeHTTP/1.1 429 Too Many RequestsContent-Type: application/json
{"error":"rate_limited"}On every CI run, the case test/t/cases/guides.rb sends this request and compares the status line, the headers shown on this page and the whole body. The five files last changed on 2026-10-29 in commit 32219e53.
The case sends the request twice. The first answers 200 with the body
accepted; the second, within the same minute, gets the response above.
What to keep in mind
Section titled “What to keep in mind”mruby_setruns in the rewrite phase, andlimit_reqin the preaccess phase after it (Request phases), so the variable has its value when nginx counts the request.- The counters live in nginx’s shared memory zone, not in Ruby. Each worker has its own mruby state, and a counter in a Ruby global would count the requests of one worker only.
limit_req_zoneis a directive of thehttpblock;limit_reqandlimit_req_statusgo into thehttpblock, aserveror alocation.