# lua-resty-limit-traffic rate, connection and bandwidth limits for OpenResty

> lua-resty-limit-traffic is a Lua library for OpenResty that limits and controls traffic in three ways, by request rate, by concurrent connections and by bandwidth, with state held in a shared memory dictionary. The design is that a request over the limit is delayed rather than dropped, and only the overflow is rejected, which is a different behaviour from the hard rejection most rate limiters implement.

**openresty/lua-resty-limit-traffic** — Lua library for limiting and controlling traffic in OpenResty/ngx_lua

- Repository: https://github.com/openresty/lua-resty-limit-traffic
- Stars: 853 · Forks: 156
- Language: Lua
- License: not declared
- Published: 2026-10-20 · Updated: 2026-10-20 · Language: en
- Canonical page: https://hysenlabs.com/projects/openresty-lua-resty-limit-traffic

## Three kinds of limit in one library

The project is described in one line as a Lua library for limiting and controlling traffic in OpenResty and ngx_lua, and the README's examples are organised by module rather than by feature.

The first is `resty.limit.req`, which limits the request rate. The second is `resty.limit.conn`, which limits the number of concurrent connections. The third is `resty.limit.traffic`, which limits traffic, that is bandwidth. Each is a separate module you require and instantiate, and each is demonstrated separately in the README, with the comment on the second example noting that it demonstrates the usage of the `resty.limit.conn` module alone.

That structure is the practical point. The three limits answer different questions, and a service usually needs more than one of them at once: a rate limit stops a client from hammering an endpoint, a connection limit stops one client from occupying every worker, and a traffic limit stops a single transfer from eating the pipe. Having them in one library with one state model means one shared dictionary per limiter rather than three different mechanisms.

The primary language is Lua, the default branch is `master`, and the repository is not archived, with the last push on 2026-09-16. The only recorded release is v0.07 from 2020-07-06, and the repository carries no recognised licence identifier, which is one of the things to sort out before adopting it.

## The shared dictionary is where the counters live

Every example starts by declaring a shared memory dictionary in the nginx configuration, and that declaration is the whole state model of the library.

```nginx
    lua_shared_dict my_limit_req_store 100m;

    server {
        location / {
            access_by_lua_block {
                -- well, we could put the require() and new() calls in our own Lua
                -- modules to save overhead. here we put them below just for
                -- convenience.

                local limit_req = require "resty.limit.req"

                -- limit the requests under 200 req/sec with a burst of 100 req/sec,
                -- that is, we delay requests under 300 req/sec and above 200
                -- req/sec, and reject any requests exceeding 300 req/sec.
                local lim, err = limit_req.new("my_limit_req_store", 200, 100)
```

The name of the dictionary is passed to `new`, and the two numbers are the rate and the burst. The comment states the semantics precisely: requests under 200 per second are not affected, requests between 200 and 300 per second are delayed, and requests above 300 per second are rejected.

Because the state lives in a shared dictionary, it is shared across the worker processes of one nginx instance and is not shared between instances. A deployment with several machines behind a load balancer therefore has one counter set per machine, and the effective limit is the per-machine limit multiplied by the number of machines. The README does not discuss that, and it is the first thing to think about before deploying it.

The 100 megabyte size in the example is also worth noting. The dictionary has to hold an entry per limiting key, so with a key per IP address the memory footprint grows with the number of distinct clients, and the shared dictionary has a fixed size.

## Over the limit means delayed, not dropped

The second half of the request rate example is the behaviour that distinguishes this library, and it is worth reading line by line.

```nginx
                -- the following call must be per-request.
                -- here we use the remote (IP) address as the limiting key
                local key = ngx.var.binary_remote_addr
                local delay, err = lim:incoming(key, true)
                if not delay then
                    if err == "rejected" then
                        return ngx.exit(503)
                    end
                    ngx.log(ngx.ERR, "failed to limit req: ", err)
                    return ngx.exit(500)
                end

                if delay >= 0.001 then
                    -- the 2nd return value holds the number of excess requests
                    -- per second for the specified key. for example, number 31
```

Three things follow from this. The `incoming` call must happen per request, and the key is chosen by the caller, with the remote IP address used here and the second return value carrying the number of excess requests per second for that key. A rejected request gets a 503, while any other failure is logged and turned into a 500, so an internal error is never reported to the client as a rate limit.

The delay itself is applied by the application, with `ngx.sleep(delay)` in the example, and the threshold of 0.001 seconds is there to avoid sleeping for a delay so small it is not worth a context switch.

This is the design choice worth weighing. A hard limiter returns 429 or 503 the moment a client exceeds the rate. This one holds the request and releases it at the configured rate, which is what you want in front of a backend that cannot absorb a burst, and what you do not want in front of a client that has a timeout shorter than the delay. The README also notes that the code could be moved into your own Lua modules to save the overhead of the require and new calls, which matters because those calls happen on every request in the example as written.

## Connection limiting needs a leaving call in the log phase

The connection limiter is the more involved of the two demonstrated modules, because a concurrency count has to be decremented when a request finishes, and the log phase is where that happens.

The instantiation takes a fourth argument.

```nginx
                -- also, we assume a default request time of 0.5 sec, which can be
                -- dynamically adjusted by the leaving() call in log_by_lua below.
                local lim, err = limit_conn.new("my_limit_conn_store", 200, 100, 0.5)
```

So it is 200 concurrent requests, a burst of 100 extra, and an assumed request duration of half a second. The comment above the call explains what those numbers mean: requests under 200 concurrent connections are unaffected, requests between 200 and 300 are delayed, and new requests above 300 are rejected.

The decrement is the part that is easy to miss. After the delay is applied, the code stores the limiter, the key and the delay in the request context, and the log phase then reads them back and calls `leaving`, passing the latency computed as the request time minus the recorded delay.

```nginx
            log_by_lua_block {
                local ctx = ngx.ctx
                local lim = ctx.limit_conn
                if lim then
                    -- if you are using an upstream module in the content phase,
                    -- then you probably want to use $upstream_response_time
                    -- instead of ($request_time - ctx.limit_conn_delay) below.
                    local latency = tonumber(ngx.var.request_time) - ctx.limit_conn_delay
                    local key = ctx.limit_conn_key
                    assert(key)
                    local conn, err = lim:leaving(key, latency)
```

If you never call `leaving`, the concurrency counter never comes back down, and the limiter will eventually reject everything for that key. The `is_committed` call in the access phase is what decides whether the request actually took a slot, so that a request which was rejected does not leave a phantom entry behind.

The comment about using the upstream response time when the work happens in an upstream module is a small piece of advice that reflects how the latency is meant to be measured, and it is the kind of detail that separates a working configuration from one that slowly drifts.

## Installing the modules and running the test suite

The Makefile is short, and it shows the shape of the project: a set of Lua modules, an install target, and a test suite that runs inside real nginx instances.

```make
install: all
	$(INSTALL) -d $(DESTDIR)$(LUA_LIB_DIR)/resty/limit/
	$(INSTALL) lib/resty/limit/*.lua $(DESTDIR)$(LUA_LIB_DIR)/resty/limit/
```

The modules are installed as plain Lua files into a `resty/limit/` directory under the Lua library path, which is the conventional layout for a `resty` library. The variables at the top of the Makefile control where that is: the OpenResty prefix defaults to `/usr/local/openresty`, the install prefix to `/usr/local`, and the Lua library directory is derived from the Lua version, with the version line itself commented out and Lua 5.1 in effect. The `DESTDIR` is honoured, so staged installs work.

The test target is more interesting.

```make
test: all
	PATH=$(OPENRESTY_PREFIX)/nginx/sbin:$$PATH prove -I../test-nginx/lib -r t
```

The tests are run with the Perl test harness `prove`, over the `t/` directory, and the OpenResty nginx binary is prepended to the path so that the tests can start their own nginx instances. The `t/` directory in the repository is the test suite, and the `valgrind.suppress` file at the top level tells you that the tests are also expected to run under a memory checker.

The `dist.ini` file is the metadata for the distribution, and the presence of a `Makefile` target named `install` alongside a `lib/` directory is the conventional shape of an OpenResty library rather than an application.

## The README calls its own API experimental

The status section is two sentences, and they are the most important thing in the document for an adopter.

The library is already usable, though still highly experimental, and the Lua API is still in flux and may change in the near future without notice.

That combination is worth pausing on, because it is a specific contract. The code works, and the project has been carried for a long time, but the interface is explicitly not stable. A configuration that calls `limit_req.new` and `lim:incoming` today may need changes tomorrow, and the release list shows a single tag, v0.07 from 2020-07-06, while the source has continued to receive pushes as recently as 2026-09-16.

The gap between the two facts is the interesting part. The library is used in production by people who have accepted that contract, and the repository continues to receive work, but there is no release cadence to pin against and no version scheme that tells you which API you are looking at. The version in the manifest is not documented in the README, and the licence is not identified either, with no recognised licence identifier attached to the repository.

For a library whose main selling point is running inside the request path of an nginx worker, the absence of a licence statement and a stable API is the substance of the risk, not the fact that the code is old.

The remaining structure of the README is the standard OpenResty library furniture, with sections for the synopsis, the description, installation, English and Chinese mailing lists, bugs and patches, the author, and a copyright and licence section, plus a See Also list for the sibling libraries in the same family.

## Conclusion

lua-resty-limit-traffic fits an OpenResty operator who wants to smooth traffic rather than drop it, and who needs per-IP limiting that runs inside the same worker processes as the request, with no extra service in the path. Skip it if you need a hard cut-off at a specific rate, if you are not running OpenResty with shared dictionaries, or if you need a stable API you can build on. Verify first that the module version in your tree matches the API you write against, because the README calls the library highly experimental and warns the API may change without notice.

## FAQ

### What does lua-resty-limit-traffic do?

It is a Lua library for limiting and controlling traffic in OpenResty and ngx_lua, with three modules: `resty.limit.req` for request rate, `resty.limit.conn` for concurrent connections, and `resty.limit.traffic` for bandwidth. State for each limiter is held in an nginx shared memory dictionary that you declare with `lua_shared_dict`.

### What happens when a request exceeds the limit?

The request is delayed rather than dropped. The README's example limits requests to 200 per second with a burst of 100, so requests between 200 and 300 per second are delayed by `ngx.sleep(delay)` and only requests above 300 per second are rejected with a 503.

### How do I install the lua-resty-limit-traffic modules?

The Makefile has an `install` target that copies `lib/resty/limit/*.lua` into `$(DESTDIR)$(LUA_LIB_DIR)/resty/limit/`, with the OpenResty prefix defaulting to `/usr/local/openresty` and the Lua library directory derived from the prefix and the Lua version. The `test` target runs the suite with `prove` over the `t/` directory.

### Why does the connection limiter need a call in the log phase?

Because a concurrency counter has to be decremented when the request ends. The access phase stores the limiter, the key and the delay in the request context, and the `log_by_lua_block` phase reads them back and calls `lim:leaving(key, latency)`, where the latency is the request time minus the recorded delay.

### Is the lua-resty-limit-traffic API stable?

The README says the library is already usable though still highly experimental, and that the Lua API is still in flux and may change in the near future without notice. The only recorded release is v0.07 from 2020-07-06, and the repository carries no recognised licence identifier.

## Sources

- [Issues](https://github.com/openresty/lua-resty-limit-traffic/issues)
- [openresty/lua-resty-limit-traffic on GitHub](https://github.com/openresty/lua-resty-limit-traffic)
- [README](https://github.com/openresty/lua-resty-limit-traffic/blob/master/README.md)
- [Releases](https://github.com/openresty/lua-resty-limit-traffic/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/openresty-lua-resty-limit-traffic
