Skip to main content

Rate Limits Configuration

Keeptrusts supports global, per-IP, per-user, and token-consumption limits, plus byte limits for selected LLM proxy paths. The current distributed backend coordinates only the global request counter; the IP, user, and token counters remain local to each gateway process.

Quick reference

global_rate_limit:
max_requests: 1000
window_seconds: 60

ip_rate_limit:
max_requests: 100
window_seconds: 60

user_rate_limit:
max_requests: 30
window_seconds: 60
header_names: ["x-user-id"]

token_rate_limit:
max_tokens: 500000
window_seconds: 3600
scope: "global"

size_limits:
max_body_bytes: 1048576
max_response_bytes: 10485760

Global rate limit

A single counter shared by the public proxy handlers that invoke the gateway's request-state checks. Health, config, and administrative endpoints are outside this counter.

global_rate_limit:
max_requests: 1000 # required
window_seconds: 60 # required
FieldTypeRequiredDefaultDescription
max_requestsintegeryesMaximum requests per window
window_secondsintegeryesFixed window size in seconds

Runtime behavior: Atomic counter with epoch-based fixed window reset. Returns HTTP 429 Too Many Requests with Retry-After header when exceeded.

Per-IP rate limit

Independent counters per client IP address.

ip_rate_limit:
max_requests: 100 # required
window_seconds: 60 # required
trust_proxy_depth: 1 # optional, default: 0
FieldTypeRequiredDefaultDescription
max_requestsintegeryesMaximum requests per IP per window
window_secondsintegeryesWindow size in seconds
trust_proxy_depthintegerno0Number of X-Forwarded-For hops to trust. 0 uses the direct connection IP

Behind a reverse proxy

When running behind nginx or a load balancer, set trust_proxy_depth to the number of trusted proxies in the chain:

# Gateway behind one nginx reverse proxy
ip_rate_limit:
max_requests: 50
window_seconds: 60
trust_proxy_depth: 1

Per-user rate limit

Independent counters per user identity extracted from request headers.

user_rate_limit:
max_requests: 30 # required
window_seconds: 60 # required
header_names: # optional
- "x-user-id"
- "x-consumer-id"
FieldTypeRequiredDefaultDescription
max_requestsintegeryesMaximum requests per user per window
window_secondsintegeryesWindow size in seconds
header_namesstring[]no["x-user-id"]Headers to extract user identity from. First non-empty value wins

Requests without a non-empty matching header bypass the per-user counter; they are not grouped into an unknown bucket. Require or inject a trusted identity header at the gateway boundary when this limit is intended to cover every request.

Token rate limit

Sliding-window limit on token consumption reported by buffered Chat Completions responses.

token_rate_limit:
max_tokens: 500000 # required
window_seconds: 3600 # required
scope: "global" # optional: global | per_key | per_ip
FieldTypeRequiredDefaultDescription
max_tokensintegeryesMaximum tokens consumed per window
window_secondsintegeryesSliding window size in seconds
scopestringno"global"Bucketing scope: global, per_key, or per_ip

Runtime behavior: Uses a 6-sub-window sliding window on buffered POST /v1/chat/completions traffic. Tokens are recorded after a successful upstream response when its JSON body contains usage.total_tokens; the next Chat Completions request is checked against the remaining budget and receives HTTP 429 when the budget is exhausted. Streaming responses and other request families are not checked or recorded by this limiter.

Scope examples

# Global: one bucket for all accounted buffered Chat Completions traffic
token_rate_limit:
max_tokens: 1000000
window_seconds: 3600
scope: "global"

# Per API key: each key gets its own budget
token_rate_limit:
max_tokens: 100000
window_seconds: 3600
scope: "per_key"

# Per IP: each direct socket peer gets its own budget
token_rate_limit:
max_tokens: 50000
window_seconds: 3600
scope: "per_ip"

token_rate_limit.scope: per_ip uses the direct socket peer address. It does not use X-Forwarded-For or ip_rate_limit.trust_proxy_depth; behind a reverse proxy, callers can therefore share the proxy's token bucket.

Size limits

Byte-level limits for the current LLM proxy implementation.

size_limits:
max_body_bytes: 1048576 # 1 MB request body
max_header_bytes: 8192 # 8 KB headers
max_url_bytes: 4096 # 4 KB URL
max_response_bytes: 10485760 # 10 MB response
FieldTypeRequiredDefaultDescription
max_body_bytesintegernounlimitedMaximum request body size
max_header_bytesintegernounlimitedMaximum total header size
max_url_bytesintegernounlimitedMaximum URL length
max_response_bytesintegernounlimitedMaximum response body size

The current request-side check runs on POST /v1/chat/completions and evaluates body, headers, then URL. Exceeding one of those request limits returns HTTP 413 Payload Too Large. Do not assume these three request-side fields protect other gateway routes.

max_response_bytes is enforced separately on the buffered Chat Completions and Responses paths. An oversized buffered upstream response returns HTTP 502 Bad Gateway with error code response_size_exceeded. For a streaming response, the HTTP headers have already been sent; the gateway emits a terminal SSE error with that code and aborts the stream.

Consumer-group request limits are configured under consumer_groups.groups[].rate_limit.max_requests. The schema also accepts max_tokens there, but live consumer enforcement does not read it. Size limits are top-level gateway limits.

Distributed rate limiting

By default, rate limit counters are per-process and in memory. A Redis or Valkey backend can coordinate the global_rate_limit request counter across gateway instances. It does not distribute the per-IP, per-user, or token counters.

Inline configuration

distributed_rate_limit:
backend: "redis"
url_env: "KEEPTRUSTS_LLM_CACHE_REDIS_URL"
FieldTypeRequiredDefaultDescription
backendstringyesredis or valkey (both use Redis wire protocol)
url_envstringno"KEEPTRUSTS_LLM_CACHE_REDIS_URL"Environment variable containing the connection URL

global_rate_limit must also be present because its max_requests and window_seconds define the single shared counter. Keep the backend in the top-level distributed_rate_limit block shown above. The runtime loader contains parser paths for nested distributed blocks, but the published configuration schema rejects those placements, and dispatch does not create distributed IP or token counters from them.

Build and connection behavior

Distributed rate limiting requires a CLI built with the distributed feature. If a backend is configured without that feature, gateway startup fails with an error instead of accepting traffic.

With the feature enabled, an empty url_env value or a backend initialization failure produces a warning and starts the gateway with local-only state. Check startup logs and runtime readiness before treating a multi-instance limit as shared.

Environment variable wiring

Set the environment variable named in url_env to the Redis or Valkey connection string before starting the gateway.

Complete rate limiting example

pack:
name: "rate-limited-gateway"
version: "1.0.0"
enabled: true

# Global ceiling
global_rate_limit:
max_requests: 5000
window_seconds: 60

# Per-IP protection
ip_rate_limit:
max_requests: 100
window_seconds: 60
trust_proxy_depth: 1

# Per-user fairness
user_rate_limit:
max_requests: 30
window_seconds: 60
header_names: ["x-user-id", "x-consumer-id"]

# Token budget
token_rate_limit:
max_tokens: 1000000
window_seconds: 3600
scope: "global"

# Request size protection
size_limits:
max_body_bytes: 2097152 # 2 MB
max_response_bytes: 20971520 # 20 MB

# Shared global request counter
distributed_rate_limit:
backend: "valkey"
url_env: "KEEPTRUSTS_LLM_CACHE_REDIS_URL"

providers:
targets:
- id: "openai-prod"
provider: "openai"
model: "your-openai-model"
secret_key_ref:
env: "KEEPTRUSTS_OPENAI_API_KEY"

policies:
chain:
- "audit-logger"

Replace your-openai-model with a model identifier available to your OpenAI account.

Next steps