GioJSdocs
On this page

[[rate_limits]]

Token-bucket request budgets per client and path, enforced in Rust before routing: a refused request gets 429 and never reaches Node.

gio.toml
[[rate_limits]]
path = "/api/*rest"
per_ip = 100
window_seconds = 60
burst = 20

[[rate_limits]]
path = "/api/login"
per_ip = 5
window_seconds = 60
burst = 0

There are no rate limits by default. Each table is one rule; add as many as you need.

Reference

KeyDefaultDescription
path (required)string-The paths the rule covers, in the pattern syntax the other path rules use: literal segments, :param for one segment, and a trailing *rest catch-all (/api/*rest, which also covers /api itself). Without a catch-all the match is exact (/api/login). The older spellings still work: /api/* is the same as /api/*rest, and /api* covers every path starting with /api (/apiary too). A path that cannot be parsed stops startup.
per_ipinteger100Requests per window_seconds per client: the bucket refills at this rate.0 / false / empty: No refill: only burst requests, ever
window_secondsinteger60The window per_ip is counted over.0 / false / empty: Treated as 1 second
burstinteger20Extra requests on top of per_ip: a new client's bucket holds per_ip + burst tokens.
key_headerstring-Key buckets on this request header's value (an API key) as well as the client. Case-insensitive name; the first 64 bytes of the value count. A request without the header uses the client's own bucket.
max_keys_per_clientinteger64With key_header: distinct header values one client may hold a budget for. Further values share the client's own bucket, so rotating the header cannot mint fresh budgets. Raise it, or set 0, for an API gateway whose many keys arrive from one address.0 / false / empty: Unlimited. Warns.

The total number of live buckets is capped by [server] rate_limit_max_buckets (100000).

Behavior

  • One rule per request. The rule whose segments cover most of the path before its wildcard wins; on a tie an exact rule beats a wildcard, then more literal text beats a :param (/users/me over /users/:id). Above, /api/login uses its own rule and every other /api path the general one.
  • Before the rules. Limits run before the CSRF check, guards, redirects and rewrites, so a flood of refused or redirected requests still spends budget. With [i18n] on, they match the path without its locale prefix: a /api/*rest rule also covers /de/api/x.
  • Canonical paths. Requests are matched with repeated and trailing slashes collapsed and unreserved escapes decoded, so /api/login/, //api//login and /api/%6Cogin share one bucket.
  • A client is an IPv4 address or an IPv6 /64 (IPv6 hosts control a whole /64), after [server] trusted_proxies resolution.
  • Headers. An admitted response carries X-RateLimit-Limit, the bucket's size (per_ip + burst), and X-RateLimit-Remaining, the requests left in it (at most the limit). A refused one looks like this, for a per_ip = 3, burst = 0 rule (other headers left out):
text
HTTP/1.1 429 Too Many Requests
content-type: application/json
retry-after: 20
x-ratelimit-limit: 3
x-ratelimit-remaining: 0
x-gio-refused: unread

{"error":"rate limit exceeded"}
  • Retry-After is window_seconds / per_ip (at least 1): the time one token takes to come back. x-gio-refused: unread tells <GioForm> the submission never reached the app.
  • Rust's own /_gio endpoints are not limited, except /_gio/image, the most expensive one. A public/ file answered at the site root is also held to rules written for its /public/... URL, charged once.
  • Each refusal is logged at warn with the client, path and rule, and counted in the metrics.

Startup errors

A path that cannot be parsed stops startup, and --check-config reports it: one that does not start with /, a *rest that is not the last segment, or a * anywhere but the end.

text
gio.toml:12: invalid `rate_limits[1].path`: path "api/*rest" must start with '/'

Startup warnings

WhenStartup warning
max_keys_per_client = 0 on a rule with key_header[[rate_limits]] /api/*: max_keys_per_client = 0 - one client can mint a fresh budget for every key_header value it sends
[server] rate_limit_max_buckets = 0 with any rule[server] rate_limit_max_buckets = 0: rate-limit buckets are never evicted - clients rotating addresses grow memory without bound

Examples

Slow down login attempts

gio.toml
[[rate_limits]]
path = "/login"
per_ip = 10
window_seconds = 900      # 10 per 15 minutes
burst = 0

Per API key budgets

gio.toml
[[rate_limits]]
path = "/api/*rest"
per_ip = 600
window_seconds = 60
key_header = "x-api-key"
max_keys_per_client = 256   # a partner's gateway sends many keys from one address

Protect the image optimizer

gio.toml
[[rate_limits]]
path = "/_gio/image"
per_ip = 120
window_seconds = 60

Good to know

  • per_ip = 0 is not an off switch: with burst = 0 it refuses every request to the path. Remove the rule to stop limiting.
  • Buckets that have refilled are dropped every minute; a new bucket starts full, so nothing is lost.
  • Limits are per server instance. Behind a load balancer each instance counts on its own.

Version history

VersionChanges
v0.1.0-beta.8Added max_keys_per_client. path takes the rule pattern syntax (:param, *rest; a /api/*rest rule used to match nothing), and a path that cannot be parsed stops startup. Paths match the canonical request path, and /api/* also covers /api. Clients are resolved through trusted proxies, and the bucket store is capped by [server] rate_limit_max_buckets. X-RateLimit-Limit is per_ip + burst (it was per_ip, below what X-RateLimit-Remaining could show), and a refusal carries X-Gio-Cache: bypass.
v0.1.0-beta.6/_gio/image honors the rules.
v0.1.0-beta.1Introduced with path, per_ip, window_seconds, burst and key_header.