Upgrading from MTE Relay v4 to v5
MTE Relay v5 replaces the v4 wire protocol, so this is a coordinated upgrade of servers and clients. This page covers what changed, how v4 settings and error codes map to v5, the steps for each client and server type, a cutover plan that keeps v4 running until you are done, and the v4 support policy.
The current v5 release is 5.0.5 for both MTE Relay Server (MRS) and
MTE API Relay (MAR).
Why v5
- Every frame is verified before any plaintext is released. v5 carries each request and response as frames, and each DATA frame is one complete MTE or MKE operation checked before its bytes move on. In the v4 MKE streaming path, decrypted chunks were passed along before the final integrity check.
- The method, status, and end of a body are bound to the ciphertext. The
encrypted metadata repeats the HTTP method and the response status that the
frame also carries in plain, and every DATA frame encrypts a copy of its
flags, so the end of a body cannot be forged. The relay refuses a request
whose encrypted and plain values disagree with
483 header_mismatch, and the client refuses such a response. Request and response headers travel in the encrypted metadata, except headers you choose to send plain and, whileforward_browser_cookiesis on (the default), the browser'sCookieand the origin'sSet-Cookie, which ride the plain HTTP request and response. - Kyber 1024 by default. v4 used Kyber 512. v5 accepts 512, 768, or 1024 per deployment.
- Errors your infrastructure can carry. Relay errors move from v4's custom
559 to 570 range to 470 to 490, each with a reason token, a registered client
action, and an
X-MTE-Relay-Errorheader that survives a replaced body. - Clients read the relay instead of hardcoding it. A discovery document at auth advertises the relay's limits, timers, and per domain settings.
- Idle clients stay paired. A keepalive endpoint refreshes every pair of a client in one call. v4 had no keepalive.
- Guard rails. Rate limits on auth, pairing, and keepalive, a request body cap, and bounded frames. v4 had no rate limits and no body cap.
- A control plane. An optional admin panel and API configure a whole fleet, with live updates, history, and rollback.
See What is new in v5 for the full list.
The breaking change
v5 speaks frame version 2, a binary protocol v4 does not understand, and there is no dual stack mode: a v5 relay serves only v5 clients, and a v4 relay serves only v4 clients.
- A v4 client starts every session with
HEAD /api/mte-relay. A v5 relay answers that probe with478 frame_version_unsupportedand never issues a token, so the v4 client never gets a session. A response toHEADcarries no body, so the error arrives only as the status and theX-MTE-Relay-Errorheader. The relay logs the minimum SDK versions at warn level. - An auth request without
frameVersions=2, or a request body in the frame version 1 layout used by the earlier v5 betas, gets478 frame_version_unsupported, and the message names the minimum SDK versions. - A v5 client cannot pair with a v4 relay either.
- The same holds between relays: a v4 MAR cannot chain to a v5 MAR.
So servers and clients upgrade together, per application. The cutover plan keeps both versions running side by side on different hostnames so each application moves when its new client ships.
Settings: v4 to v5
v5 routes everything through DOMAIN_MAP. The v4 single upstream mode and its
variables are gone. See the environment variables
reference for every v5 setting.
| v4 setting | v4 default | v5 | Notes |
|---|---|---|---|
UPSTREAM | none | removed | Use a DOMAIN_MAP entry with upstream. |
CLIENT_ID_SECRET | none | client_id_secret per domain | Set the same value on every replica. |
CORS_ORIGINS | none | cors_origins per domain | |
CORS_METHODS | GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS, CONNECT, TRACE | cors_methods per domain | The v5 default is GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD. |
PASS_THROUGH_ROUTES | none | pass_through_routes per domain | v4 entries were regular expressions. v5 entries are exact paths or prefixes ending in *. |
HEADERS | none | headers per domain | v5 sets these on upstream requests only. v4 also set them on responses to the client. |
OUTBOUND_TOKEN, OUTBOUND_TOKEN_HEADER | none, x-mte-outbound-token | removed | An outbound domain is a DOMAIN_MAP entry with outbound: true. |
DOMAIN_MAP | none | DOMAIN_MAP | Same idea, many more fields. outbound_token is removed. Keys and the request Host now compare case insensitively, and a map with two keys that differ only by letter case is refused. See DOMAIN_MAP. |
POOL_SIZE | 600, capped at 1000 | DEFAULT_POOL_SIZE 250, capped by MAX_POOL_SIZE_LIMIT 100000 | |
OUTBOUND_PAIR_COUNT | 1024 | CLIENT_INITIAL_PAIRS 2, CLIENT_MIN_PAIRS 25 | v5 creates a few pairs up front and fills to the minimum in the background. |
OUTBOUND_PAIR_GROW_COUNT | 256 | CLIENT_REFILL_THRESHOLD 10 | v5 refills when available pairs fall to the threshold or below, or when requests are waiting for a pair, and sizes each refill to the requests waiting. |
OUTBOUND_MAX_PAIR_COUNT | 8192 | CLIENT_MAX_PAIRS 1024 Since 5.0.1 | The ceiling changed. Both versions hold one pair per request in flight, so this is the concurrency one outbound replica carries through one outbound host. Size it to your peak, and set MAX_PAIRS_PER_CLIENT on the inbound relay to at least the same value. Relay 5.0.0 shipped with 100; see Limits and scaling for the 5.0.0 settings. |
OUTBOUND_ACQUIRE_TIMEOUT | 5s | CLIENT_PAIR_ACQUIRE_WAIT_MS 1000 Since 5.0.1 | Milliseconds in v5. Past the wait the caller gets 485 upstream_capacity with Retry-After. Relay 5.0.0 shipped with 250. |
FORWARD_HOST_HEADER | true | FORWARD_HEADERS true | v5 turns X-Forwarded-Host, X-Forwarded-Proto, and X-Forwarded-For on or off together, and the setting is live. |
RESEED_THRESHOLD | 90 | removed | |
BILLING_STRATEGY, BILLING_PROVIDER, BILLING_PRODUCT, BILLING_REPORT_INTERVAL | same names, set by the image | The AWS Marketplace images set the first three for their listing; leave them as the image sets them. The other images ship with billing off. BILLING_REPORT_INTERVAL has no effect on the relay. | |
PERF_STATS_INTERVAL | 10s | PERF_STATS_INTERVAL 10 | A v4 duration such as 10s is not an integer, so v5 ignores it and uses 10 seconds. Write plain seconds. |
COMPANY_NAME, LICENSE_KEY | required | same names | v5 falls back to a development license when they are unset. Set your production license. |
PORT | 8080 | LISTEN_PORT 8080 | PORT still works as an alias, so a 4.x file needs no change. LISTEN_PORT wins when both are set, and a disagreement is logged once. The port must be from 1 to 65535. |
HOSTNAME | 0.0.0.0 | retired | It never bound anything in 4.x or 5.x. v5 ignores it with a warning, except the pod or container name a runtime sets, which never warns. To bind one interface, set the new LISTEN_HOST. |
LOG_LEVEL | info | same name | Accepts trace, debug, info, warn, warning, error, fatal, panic, or disabled, in any letter case. An unknown value now logs a warning and falls back to info, where v4 refused to start. |
USE_CONSOLE_LOGS, REDIS_URL, HTTP_MAX_IDLE_CONNS_PER_HOST, ENABLE_PPROF, PPROF_PORT, MUTEX_PROFILE_FRACTION, BLOCK_PROFILE_RATE, ENABLE_PERF_STATS | same names | Booleans accept true, 1, yes, on and false, 0, no, off in any letter case. A value that does not parse logs a warning and takes the default. |
New in v5, with no v4 equivalent: LISTEN_HOST, MAX_REQUEST_BODY_MIB,
MAX_CONTROL_BODY_MIB, KYBER_STRENGTH, USE_MKE, SEQUENCE_WINDOW,
TIME_WINDOW, SESSION_TIMEOUT_MINUTES, HTTP_CLIENT_TIMEOUT_SECONDS, the
protocol bounds and timers (MAX_PAIR_BYTES among them, with MAX_PAIR_MIB
as an alias), RATE_LIMITS, TRUSTED_PROXIES, the Redis tuning variables,
LOCAL_STATE_CACHE, HEALTH_CHECK_PATH, and the control plane and probe
settings. Every setting v5 shares with SocketX has the same name, default,
and parsing rule in both products.
Converting a v4 configuration
A v4 single upstream configuration such as:
UPSTREAM=https://api.example.com
CLIENT_ID_SECRET=REPLACE_WITH_A_LONG_RANDOM_SECRET
CORS_ORIGINS=https://app.example.com
PASS_THROUGH_ROUTES=/health,/version
becomes one DOMAIN_MAP entry keyed by the host clients call:
{
"relay.example.com": {
"upstream": "https://api.example.com",
"client_id_secret": "REPLACE_WITH_A_LONG_RANDOM_SECRET",
"cors_origins": ["https://app.example.com"],
"pass_through_routes": ["/health", "/version"]
}
}
Check every v4 pass through entry. v4 matched each entry as a regular
expression anywhere in the path, so /health also matched
/v1/health/live. v5 matches /health exactly, and a prefix needs a
trailing *: ^/public/.* becomes /public/*. An entry v5 cannot express as
an exact path or a prefix needs one entry per path.
For MTE API Relay, a v4 outbound entry keyed on outbound_token becomes an
outbound domain. Your service
stops sending x-mte-upstream and x-mte-outbound-token and calls the outbound
relay directly; the entry's upstream names the next relay.
Error codes: v4 to v5
v4 4.8.0 sends 560 to 563, 565, 566, and 569 from the relay, plus 503 when
an outbound pair pool is exhausted. It defines 559, 564, 567, and 568 but never
sends them, and 570 covers an item that is neither a string nor bytes, which no
v4 code path passes. v5 never sends any of those custom codes. Move alerts and
client handling to the v5 codes:
| v4 code | v4 meaning | Closest v5 answer |
|---|---|---|
| 560 | State not found | 470 pair_not_found, or 475 invalid_client_id when the relay holds no pairs for the client |
| 561 | Failed to encode | 474 encode_failed |
| 562 | Failed to decode | 473 decode_failed |
| 563 | Failed to get state, raised when saving encoder or decoder state | 503 state_store_write_failed |
| 564 | DRBG reseed required, never sent | none; the browser engine retires a pair before its reseed limit |
| 565 | MTE status not successful, raised when restoring state, and on an MAR when the pair pool cannot grow or the next relay fails | 471 decoder_restore_failed or 472 encoder_restore_failed; on an MAR chain, 502 with the next relay's code as its cause |
| 566 | Invalid client ID header | 475 invalid_client_id, or 475 invalid_token for an expired token |
| 569 | Missing required header | 477 invalid_frame or 490 invalid_request; v5 has no required plaintext relay headers |
| 570 | Item was not a byte array or string | 500 internal |
| 503 (MAR) | MTE pair pool exhausted | 485 upstream_capacity |
The mapping is by meaning, not one to one. v5 codes carry a reason token and a registered client action, listed on Errors and recovery.
Upgrading clients
Every client platform needs a v5 SDK.
Web (JavaScript)
-
Install the v5 package:
npm install mte-relay-browser-public-client@5.0.0It bundles the
mte-relay-browserengine 5.0.0, which speaks frame version 2. -
Keep your
mteFetchcalls. The request URL still names the relay origin. -
Remove v4 per request options. v5
mteFetchaccepts onlyencodeTypeandunencryptedHeadersin its third argument. The v4 optionsencodeUrl,encodeHeaders,pathPrefix, anduseStreamingare gone: the path and every header are encrypted unless listed inunencryptedHeaders, and responses stream. -
Move startup options to
configureMteRelay(options), called once before the firstmteFetch. -
Update error handling to the v5 error classes, which carry the registry
code,reason, and recoveryaction.
The JavaScript client page has the full v5 API.
Angular
Install mte-relay-angular@5.0.0, which runs on the
mte-relay-browser engine 5.0.0 and speaks frame version 2. See the
Angular client.
iOS, Android, and Flutter
v5 iOS, Android, and Flutter clients speak frame version 2, and a v4 mobile SDK cannot connect to a v5 relay. Install the release for your client:
- Swift / iOS:
5.3.0. - Java / Android:
5.3.0. - Flutter:
5.3.0.
Ship a new app build. Apps already in users' hands keep using v4 until they update, which is the main reason to run v4 and v5 side by side during the cutover.
MTE API Relay callers
A MAR chain has no client SDK. Upgrade both relays of the chain to the v5 MAR
image together, convert the outbound entries to outbound: true domains, and
change the calling service to send ordinary requests to the outbound relay, with
no x-mte-* headers.
Upgrading servers
Before you start
- Get access to the v5 images:
321186633847.dkr.ecr.us-east-1.amazonaws.com/customer/on-prem/mte-relay-server:5.0.5for MTE Relay Server and321186633847.dkr.ecr.us-east-1.amazonaws.com/customer/on-prem/mte-api-relay:5.0.5for MTE API Relay. - Convert the configuration with the tables above.
- Set an explicit
client_id_secretfor every domain. - Give v5 its own Redis instance or database when you run two or more replicas. v4 pair state cannot be used by v5, so there is nothing to migrate; clients pair again.
Docker and Docker Compose
- Add a v5 service beside the v4 one, on its own host port or hostname, with
the v5 image and a
DOMAIN_MAP. - Check its health from outside the container, for example from a load
balancer or monitor: the probe listener on port 9091 (
GET /healthzandGET /readyz), or on the relay portGET /api/mte-echoor the path you set inHEALTH_CHECK_PATH. The image is distroless, so a composehealthcheckcommand cannot run inside it. - Start it and run the validation checklist.
See the MTE Relay Server on premise guide and the MTE API Relay on premise guide for complete files.
Kubernetes and OpenShift
- Deploy v5 as a separate Deployment and Service, not as a new image on the v4 Deployment, because a rolling update would mix protocol versions behind one Service.
- Use the probe listener: readiness on
GET /readyzand liveness onGET /healthz, port 9091. - Expose it on a new hostname with its own Ingress or Route.
- Optionally add the control plane, in
which case the workers need only
CONTROL_PLANE_URLandCONTROL_PLANE_TOKEN.
AWS, Azure, GCP, and Oracle Cloud
The v5 relay runs as the same container on every platform. Deploy a new v5 service beside the v4 one on your container platform (ECS, EKS, AKS, GKE, OKE, or a container instance service) behind its own hostname, then follow the cutover plan. The platform guides under MTE Relay Server and MTE API Relay have examples.
WAF, CORS, and monitoring
- Allow
application/octet-streamrequest bodies on the v5 hostname. v4 put ciphertext in the URL and custom headers; v5 sends a binaryPOST /. See CORS and WAF. - If a domain customizes
cors_allowed_headers, includeContent-TypeandX-MTE-Relay-Route. The v4 headersx-mte-relayandx-mte-relay-ehare gone. - Check each
cors_originsentry against the v5 origin rules: matching is case insensitive,*.example.comaccepts any scheme, andhttps://*.example.comrequires the scheme to match, as in v4. v4 refused a disallowedOriginwith403; v5 answers it without CORS headers, which a browser cannot read, and curl and native clients are unaffected. See CORS and WAF. - Move alerts from 559 to 570 to 470 to 490 and the
X-MTE-Relay-Errorheader. Keep 500, 502, and 503 as true fault signals. - Health checks need no change.
GET /api/mte-echoanswers as in v4, the message is still a JSON string, and v5 also takesHEAD /api/mte-echo. For a load balancer or a security scanner that can only reach the relay port, the new optionalHEALTH_CHECK_PATHmounts a200{"status":"ready"}answer at a path you pick, for anyHostand outside every rate limit. It is off by default. See CORS and WAF.
Cutover plan
The low risk path runs v5 beside v4 and moves clients over one application at a time.
- Stand up v5 on a separate hostname, for example
relay-v5.example.com, in front of the same upstream, with its own replicas and Redis. Leave v4 untouched. - Validate v5 with the checklist below and a v5 client build pointed at the new hostname.
- Ship clients that use the v5 SDK and the v5 hostname. Web applications switch when you deploy. Mobile applications switch as users update, so v4 keeps serving older app versions meanwhile.
- Watch both. On v5, watch the 470 to 490 codes, latency, and upstream errors. On v4, watch traffic fall.
- Retire v4 when its traffic has stopped, and before October 1, 2027, when v4 support ends.
For an MTE API Relay chain, stand up a v5 pair of relays beside the v4 pair, point the calling service at the v5 outbound relay, and retire the v4 pair once traffic has moved.
Reusing the v4 hostname for v5 in one step is possible, but every client of that hostname must switch at the same moment, and any v4 client left behind fails until it updates.
Validation checklist
Replace the host and ports with your own. Send the Host header your
DOMAIN_MAP expects.
-
The relay answers its echo endpoint:
curl -s http://localhost:8080/api/mte-echo# {"message":"true","timestamp":"2026-01-04T10:30:45.123456789Z"} -
The worker is ready:
curl -s http://localhost:9091/readyz# {"status":"ready","phase":"ready"} -
Auth returns a v5 discovery document with the values you expect:
curl -s -H "Host: relay-v5.example.com" \"http://localhost:8080/api/mte-relay?frameVersions=2" | jq .relayCheck
frameVersions([2]),kyberStrength,maxRequestBodyBytes, andpassThroughRoutes. -
The same request without
?frameVersions=2returns478, which proves the host reaches a v5 relay. -
A v5 client request reaches the upstream, and the upstream logs show the expected
X-Forwarded-Host. -
Browser clients show no CORS errors and no
MteRelaySetupError. -
With two or more replicas, traffic spreads across them without a rise in
475or470. -
Dashboards and alerts see the 470 to 490 codes and the
X-MTE-Relay-Errorheader. -
With the control plane, every worker shows as in sync on the Fleet page.
Rollback
- Before clients move, rollback is removing the v5 service. v4 never stopped.
- Web clients roll back by redeploying the previous v4 build, which still points at the v4 hostname.
- Mobile clients cannot be rolled back once installed, and a v5 client cannot talk to a v4 relay. Keep the v5 relay running and fix forward, and ship mobile clients only after the v5 relay passes validation.
- A bad v5 configuration rolls back from the control plane History page, or by redeploying the previous environment values.
- A bad v5 release rolls back by redeploying the previous v5 image tag.
v4 support policy
- MTE Relay v4 is supported until October 1, 2027.
- v4 images stay available at least until then.
- v4 images get no updates after October 1, 2027.
- The v4 documentation stays online: MTE Relay v4 documentation.