---
title: "Limit the rate per API key"
language: "en"
documentation_version: "3.x"
channel: "development"
release_context: null
source_revision: "10297d1e28653321d6996609fc53718d3b7c8278"
working_tree_clean: true
source_path: "docs/guides/rate-limit.md"
source_sha256: "6560a864e27e246632b7ee2007b24a9c8b62962bdbbdd8fd709ac1fa5fbeeeee"
source_at_revision: true
source_url: "https://github.com/matsumotory/ngx_mruby/blob/10297d1e28653321d6996609fc53718d3b7c8278/docs/guides/rate-limit.md"
html_url: "https://ngx.mruby.org/guides/rate-limit/"
markdown_url: "https://ngx.mruby.org/markdown/guides/rate-limit.md"
last_updated: "2026-10-29"
---

# 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`](/reference/directives/#mruby_set-and-mruby_set_code), 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

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.

```nginx title="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';
    }
}
```

```ruby title="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}"
```

```http title="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
```

```http title="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"}
```

<p class="caption sample-caption">On every CI run, the case <code>test/t/cases/guides.rb</code> sends this request and compares the status line, the headers shown on this page and the whole body. The five files last changed on <span class="nowrap">2026-10-29</span> in commit <code class="nowrap">32219e53</code>.</p>

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

- `mruby_set` runs in the rewrite phase, and `limit_req` in the preaccess
  phase after it ([Request phases](/concepts/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`.
