Skip to main content
Version: 5.x

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 answered 477 eof_before_end with 481 as 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_large on 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, with MteRelayRequestTooLargeError, which carries size and limit.

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​

SettingDefaultOver the bound
MAX_FRAME_BYTES65536481 frame_too_large
MAX_METADATA_BYTES_MKE61439Request metadata (method, path, headers): 482 metadata_invalid. Origin response headers: 502 upstream_response_failed.
MAX_METADATA_BYTES_MTE7168Same 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​

SettingDefaultOver the limit
MAX_PAIRS_PER_BATCH100490 invalid_request, with a message naming maxPairsPerBatch
MAX_PAIRS_PER_CLIENT2048 Since 5.0.1490 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.

EndpointPer source addressPer client
GET /api/mte-relay (auth and discovery)600 per minutenone
POST /api/mte-pair600 per minute60 per minute
POST /api/mte-keepalivenone120 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: remoteAddr on the warn line for each 488, and origin on security events, both as ip:port. The address part is what goes in the list.
  • The setting changes only the rate limit key. The X-Forwarded-For the 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 (default 1024) is the most requests one replica can have in flight through one outbound host. A request that finds no free pair waits CLIENT_PAIR_ACQUIRE_WAIT_MS (default 1000) in arrival order, then is answered 485 upstream_capacity with Retry-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_CLIENT on the receiving relay (default 2048) must be at least CLIENT_MAX_PAIRS. The outbound relay reads it from discovery, clamps its pool to it, and logs Clamping the pool to the server's maxPairsPerClient when it is lower.
  • The pool starts at CLIENT_INITIAL_PAIRS (default 2), fills to CLIENT_MIN_PAIRS (default 25) in the background, and grows under load: each refill is sized to the requests waiting plus CLIENT_REFILL_THRESHOLD (default 10), and to at least half the pairs in use, at most MAX_PAIRS_PER_BATCH (default 100) per pairing call; the next refill starts as soon as one lands. Pairs idle past the session timeout are released back down to CLIENT_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, and Pairing rate limited in 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_PAIRS and CLIENT_MAX_PAIRS to the same value and MAX_PAIRS_PER_BATCH on the receiving relay to at least that value. One pairing call carries at most about 900 pairs at Kyber 1024 under the 4 MiB MAX_CONTROL_BODY_MIB cap, so keep that value under 900; a larger pool fills in batches of MAX_PAIRS_PER_BATCH instead.
  • 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_PAIRS above CLIENT_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:

  1. The same client_id_secret on every replica, per domain. Without it each replica generates its own secret, and a client that authenticated on one replica fails on another with 475 invalid_client_id. The control plane setup wizard generates one secret per host for you.
  2. REDIS_URL for shared pair state. Without it, a request that reaches a replica that did not create the pair fails with 470 pair_not_found, or with 475 invalid_client_id when 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.