Skip to main content

limit-count

limit-count

Counts requests per resolved key within a fixed time window and rejects those that exceed count requests per time_window seconds. Unlike the token-bucket rate-limit plugin (smooth continuous refill), this enforces a hard cap per discrete window. Counting is delegated to a shared counter backend: the in-memory local backend, or redis for cluster-shared counters via a named stores: entry. Place it before upstream to shed excess traffic early.

Configuration

KeyTypeDefaultDescription
countinteger— (required)Requests allowed per window; must be greater than 0.
time_windowinteger— (required)Window length in seconds; must be greater than 0.
keystring"$remote_addr"A $var template resolved per request (e.g. $remote_addr, $consumer_name, $http_x_api_key). An empty resolved value falls back to the client remote address.
policystringlocalCounter backend. local = per-instance in-memory windows; redis = cluster-shared windows via a named stores: entry. Anything else is rejected at config load with the supported list.
storestringRequired when policy: redis: the name of a declared stores: entry (redis or valkey). Unknown names fail policy compilation.
groupstringPrefixes the counter key so multiple nodes share one counter.
rejected_codeinteger503Status for over-limit requests (200–599).
rejected_msgstringMessage placed in the rejection body ({"error_msg": ...}).
show_limit_quota_headerbooltrueEmit X-RateLimit-Limit/-Remaining/-Reset headers onto the response.
allow_degradationboolfalseOn a counter-backend error, allow the request through instead of failing it.
type: limit-count
config:
count: 100
time_window: 60
key: "$remote_addr"
policy: local
rejected_code: 429
show_limit_quota_header: true

Behavior

The counter key is resolved by interpolating the key template against the request (supported $var names include $remote_addr, $consumer_name, $http_<header>, and $arg_<query>). When the template resolves to empty, the key falls back to the client remote address. With group set, the resolved key is prefixed with group: so several nodes count against one shared counter.

On each request the key is counted against the fixed window:

  • Within the limit — the request passes through the success port. When show_limit_quota_header is set, X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset (whole seconds until the window resets) are written onto context.response.
  • Over the limit — the plugin writes a rejection onto context.response (status rejected_code, JSON body {"error_msg": ...} using rejected_msg or a default, content-type: application/json, plus the quota headers with X-RateLimit-Remaining: 0) and exits through the limited port.

With the local policy, counts live in process memory: they are per gateway instance and are lost on restart. If the counter backend errors — a genuine infrastructure failure — the request fails with error code RATE_LIMIT_UNAVAILABLE through the error port (a 500 response is prepared) unless allow_degradation is set, in which case it passes through on success.

With policy: redis, window boundaries are wall-clock aligned (now / time_window) and shared by every gateway instance — switching from local changes boundaries from first-request-aligned to clock-aligned. Backend errors respect allow_degradation (default false: reject). Errors are counted in gateway_counter_store_errors_total{store}.

The quota headers are set on context.response; they are present when the final response is built and sent to the client.

Ports

limit-count declares three output ports: success, limited (a quota rejection is prepared), and error (a genuine counter-backend failure). success and limited are mandatory — the policy compiler rejects any policy that leaves either unwired. Wire limit-count.limited straight to client so the prepared rejection reaches the caller instead of continuing into upstream:

edges:
- from: limit-count.success
to: upstream.in
- from: limit-count.limited
to: client.in
- from: limit-count.error
to: client.in

Errors

The node returns the Context with an error, so the graph engine routes through the error port and appends the error to context.errors. The status below is the one prepared on context.response; wire error to client (or an error-handler) for the caller to see it.

CodeStatusWhen
RATE_LIMIT_UNAVAILABLE500The counter backend failed (policy: redis) — the node rejects rather than failing open.