Limits and scaling
Request body cap
MAX_REQUEST_BODY_MIB caps the plaintext request body, in MiB. The default
0 means unlimited, and the setting is live, so a control plane change applies
within one poll interval.
- The cap is inclusive: exactly the cap passes, and one byte more is answered
481 body_too_large. - On an encrypted inbound request the relay counts decoded plaintext across
the DATA frames and stops forwarding as soon as the count crosses the cap.
It then reads the rest of the body, up to 1 MiB and 5 seconds, and answers
481 body_too_large. A body that runs past that is answered477 eof_before_endwith481as its cause, and the client replaces the pair. - On an outbound domain the relay buffers the plaintext body under the cap
and answers
481 body_too_largeon the plain HTTP side. - Pass through routes are never capped.
- Discovery advertises the cap in bytes as
maxRequestBodyBytes. The browser engine refuses a larger body before sending it, withMteRelayRequestTooLargeError, which carriessizeandlimit.
MAX_CONTROL_BODY_MIB (default 4) separately caps the JSON bodies of
/api/mte-pair and /api/mte-keepalive, also with 481 body_too_large.
A proxy or ingress in front of the relay has its own body limit. Make it at least as large as your biggest upload and your clients' pairing requests.
Frame and metadata bounds
| Setting | Default | Over the bound |
|---|---|---|
MAX_FRAME_BYTES | 65536 | 481 frame_too_large |
MAX_METADATA_BYTES_MKE | 61439 | Request metadata (method, path, headers): 482 metadata_invalid. Origin response headers: 502 upstream_response_failed. |
MAX_METADATA_BYTES_MTE | 7168 | Same as MKE, for standard MTE requests. |
Clients read these from discovery and size their frames and metadata to fit.
The metadata bound also limits the response: an origin whose response headers
do not fit is answered 502 upstream_response_failed, and the relay log names
the size.
On an outbound domain, request headers too large for the frame are refused
with 490 invalid_request before anything is sent, and the message names the
header or the total size.
Pair limits
| Setting | Default | Over the limit |
|---|---|---|
MAX_PAIRS_PER_BATCH | 100 | 490 invalid_request, with a message naming maxPairsPerBatch |
MAX_PAIRS_PER_CLIENT | 2048 Since 5.0.1 | 490 pair_limit |
A pair id the client already holds is refused with 490 pair_exists. The
browser engine clamps its own pool to maxPairsPerClient.
Pair budgets
MAX_PAIR_BYTES (default 1073741824, which is 1 GiB) and MAX_PAIR_SECONDS
(default 86400) are the rekey thresholds a connection transport applies per
direction, advertised as maxPairBytes and maxPairSeconds. 0 takes the
default and never means unlimited, so a connection always rekeys.
MAX_PAIR_MIB is the 4.x alias in MiB: MAX_PAIR_BYTES wins when both are
set, and a disagreement is logged once. The HTTP relay advertises both values
and does not enforce them, because it has no connection to rekey. SocketX
reads the same two names with the same defaults and enforces them.
Rate limits
With RATE_LIMITS=true, the default, the relay limits its three control
endpoints. The data path is never rate limited.
| Endpoint | Per source address | Per client |
|---|---|---|
GET /api/mte-relay (auth and discovery) | 600 per minute | none |
POST /api/mte-pair | 600 per minute | 60 per minute |
POST /api/mte-keepalive | none | 120 per minute |
Each limit is a token bucket whose burst equals its per minute rate, so
discovery allows 600 requests per minute per source with a burst of 600, the
budget SocketX applies to its discovery endpoint as well. Over the limit the
relay answers 488 rate_limited with Retry-After and the message
too many requests; back off with jitter, and clients back off with jitter and
keep their pairs. The rates are fixed; RATE_LIMITS=false turns every token
bucket limiter off, on the relay and on SocketX alike.
Behind a load balancer
By default the per source limits key on the peer address of the connection, the address the relay sees. Behind a load balancer, ingress, or reverse proxy, that is the proxy's address, so all clients that arrive through one proxy address share one budget of 600 auth and 600 pair requests per minute. The per client limits still apply to each client id.
TRUSTED_PROXIES gives each client address its own budget again. Set it to the
addresses your proxies connect from, as a comma separated list of IP addresses
and CIDRs, for example:
TRUSTED_PROXIES=10.0.0.0/24,10.0.1.0/24
For a request whose peer is in the list, the relay reads X-Forwarded-For from
the right, skips every address in the list, and keys on the first address
outside it. If the walk runs out of entries, or reaches one that is not an IP
address, the relay keys on the last trusted address it read, which is the peer
itself when X-Forwarded-For is empty. An entry that carries a port, such as
203.0.113.7:51000 or [2001:db8::7]:51000, counts as its address. A request
from a peer outside the list keys on that peer, and its X-Forwarded-For is
ignored, so a client cannot choose its own bucket.
- List only proxies that append the client address to
X-Forwarded-For, and never a network that clients connect from. A request from a listed address can name any client address it likes. - The relay's logs keep the peer address:
remoteAddron the warn line for each488, andoriginon security events, both asip:port. The address part is what goes in the list. - The setting changes only the rate limit key. The
X-Forwarded-Forthe relay sends upstream still appends the peer address. - It is a restart setting. A list with a host name, a CIDR with host bits
set, or an entry that covers every address, such as
0.0.0.0/0, is refused; see the validation rules.
Codec pools
The relay keeps four pools of MTE instances: MTE encoders, MTE decoders, MKE
encoders, and MKE decoders. DEFAULT_POOL_SIZE (default 250) is how many
idle instances each pool keeps, and the pools fill to that size at startup. A
request that finds its pool empty creates a new instance, and an instance
returned to a full pool is discarded. MAX_POOL_SIZE_LIMIT (default 100000)
caps DEFAULT_POOL_SIZE.
The outbound relay client used by outbound domains
keeps its own pair pool, sized by CLIENT_INITIAL_PAIRS, CLIENT_MIN_PAIRS,
CLIENT_MAX_PAIRS, CLIENT_REFILL_THRESHOLD, and
CLIENT_PAIR_ACQUIRE_WAIT_MS. See the
environment variables.
Outbound pair pool Since 5.0.1
A request on an outbound domain holds one pair from the moment the relay encodes it until
its response ends, and a stream such as SSE holds its pair for the whole stream. The relay
keeps one outbound client, and so one pool, per outbound host in DOMAIN_MAP. The pool
therefore sets the concurrency one relay replica carries through one outbound host:
CLIENT_MAX_PAIRS(default1024) is the most requests one replica can have in flight through one outbound host. A request that finds no free pair waitsCLIENT_PAIR_ACQUIRE_WAIT_MS(default1000) in arrival order, then is answered485 upstream_capacitywithRetry-After. Size it to your peak concurrency per outbound host, per replica. A slow origin needs more pairs for the same request rate: at one second per request, 1,024 pairs carry about 1,000 requests a second per replica.MAX_PAIRS_PER_CLIENTon the receiving relay (default2048) must be at leastCLIENT_MAX_PAIRS. The outbound relay reads it from discovery, clamps its pool to it, and logsClamping the pool to the server's maxPairsPerClientwhen it is lower.- The pool starts at
CLIENT_INITIAL_PAIRS(default2), fills toCLIENT_MIN_PAIRS(default25) in the background, and grows under load: each refill is sized to the requests waiting plusCLIENT_REFILL_THRESHOLD(default10), and to at least half the pairs in use, at mostMAX_PAIRS_PER_BATCH(default100) per pairing call; the next refill starts as soon as one lands. Pairs idle past the session timeout are released back down toCLIENT_MIN_PAIRS. - With rate limits on, the receiving relay accepts 60 pairing calls a minute per client. A
refused pairing call pauses refills for its
Retry-After, andPairing rate limitedin the outbound relay log means growth is waiting on that limit. Growing by half the pairs in use keeps a ramp to 1,024 pairs under 20 pairing calls. - To create the whole pool in one pairing call, when the first request reaches the host, set
CLIENT_INITIAL_PAIRS,CLIENT_MIN_PAIRSandCLIENT_MAX_PAIRSto the same value andMAX_PAIRS_PER_BATCHon the receiving relay to at least that value. One pairing call carries at most about 900 pairs at Kyber 1024 under the 4 MiBMAX_CONTROL_BODY_MIBcap, so keep that value under 900; a larger pool fills in batches ofMAX_PAIRS_PER_BATCHinstead. - A caller that abandons a streamed response retires its pair (one that leaves before the
response arrives only releases it), and the pool brings retired pairs back in one pairing call
a second while free pairs remain above the threshold. A pinned pool has no room to refill ahead
of demand, so where callers often abandon streams, leave
CLIENT_MAX_PAIRSaboveCLIENT_MIN_PAIRS.
On Relay 5.0.0
Relay 5.0.0 shipped with CLIENT_MAX_PAIRS 100, MAX_PAIRS_PER_CLIENT 200 and
CLIENT_PAIR_ACQUIRE_WAIT_MS 250, and its pool grows by at most eleven pairs per refill, so
raising the ceiling alone does not carry a burst. Pin the pool instead: set
CLIENT_INITIAL_PAIRS, CLIENT_MIN_PAIRS and CLIENT_MAX_PAIRS on the outbound relay to
your peak concurrency per outbound host, at most about 900 at Kyber 1024, and
MAX_PAIRS_PER_BATCH and MAX_PAIRS_PER_CLIENT on the receiving relay to at least that
value. The whole pool is then created in one pairing call when the first request reaches the
host. On 5.0.0 every retired pair also costs its own pairing call, so above about one abandoned
stream a second per outbound host the inbound relay's pairing rate limit slows replacements.
Kyber strength
KYBER_STRENGTH defaults to 1024; v4 used 512. Both ends of a pair must use
the same strength. The relay advertises it in discovery and every pairing
request repeats it, so a mismatch fails at session setup with a clear message
instead of at the first decode. The browser engine always uses 1024, so a
relay that serves browsers keeps the default.
Running more than one replica
Two settings make a fleet behind a load balancer work:
- The same
client_id_secreton every replica, per domain. Without it each replica generates its own secret, and a client that authenticated on one replica fails on another with475 invalid_client_id. The control plane setup wizard generates one secret per host for you. REDIS_URLfor shared pair state. Without it, a request that reaches a replica that did not create the pair fails with470 pair_not_found, or with475 invalid_client_idwhen that replica holds no pairs for the client.
The relay uses a single node Redis client, so point REDIS_URL at a Redis
without cluster mode, such as ElastiCache with cluster mode disabled or a non
sharded OCI Cache cluster. A clustered Redis redirects keys held on other
shards, and the relay does not follow those redirects.
Pair state is short lived, and clients pair again after a Redis restart. The
tuning variables (REDIS_POOL_SIZE, the timeouts, REDIS_MAX_RETRIES, and
REDIS_OP_TIMEOUT_SECONDS) apply only when REDIS_URL is set.
Sticky routing and the local state cache
Every request a client sends after its first auth carries X-MTE-Relay-Route
(the browser engine also leaves it off the auth of a full repair),
its client id as 32 lowercase hex characters, and the relay echoes it on the
response. The value stays the same until the relay issues the client a new id. The value is a routing key, not a
credential. A load balancer that hashes on this header keeps each client on one
replica. With ingress-nginx, for example:
metadata:
annotations:
nginx.ingress.kubernetes.io/upstream-hash-by: "$http_x_mte_relay_route"
With that routing in place, LOCAL_STATE_CACHE=true lets each replica serve
pair state reads from memory and write them through to Redis, which saves a
Redis round trip per state read. Leave it off unless routing is sticky: without
one owner per client, a replica can serve state from memory that another
replica already advanced in Redis, and decoding fails.
When a domain customizes cors_allowed_headers, keep X-MTE-Relay-Route in
the list, or browsers fail the preflight once the relay has issued a client
id. The relay always exposes its own response headers, whatever
cors_exposed_headers says.