Skip to content

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 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.

test/conf/conf.d/75-guide-rate-limit.conf
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';
}
}
test/html/guide_rate_limit.rb
r = Nginx::Request.new
key = r.headers_in["X-API-Key"].to_s
key.empty? ? "addr:#{r.var.remote_addr}" : "key:#{key}"
test/html/guide_rate_limit.request: the request that the case sends
GET /api/orders HTTP/1.1
Host: 127.0.0.1:18166
X-API-Key: demo-key
Connection: close
test/html/guide_rate_limit.response: the response that the case compares
HTTP/1.1 429 Too Many Requests
Content-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.

  • mruby_set runs in the rewrite phase, and limit_req in 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_zone is a directive of the http block; limit_req and limit_req_status go into the http block, a server or a location.