Config
zoxy.json v0.2.1
zoxy takes exactly one argument: the path to a JSON config. It is parsed once at startup into an immutable arena — an unknown field, a duplicate key or an out-of-range number is a hard error at load, never a warning and never a surprise at request time. Changing the config is a restart, not a reload.
Generated from the config JSON Schema shipped with v0.2.1, published 13 Aug 2026. Every field, bound and default below is read out of that document — which zoxy itself generates from the types its parser uses, so nothing on this page is typed by hand.
Start here
The smallest config that does something: one L4 listener relaying TCP to one origin. It ships
in the release as config/example.json, relayed here with the $schema line added.
{ "$schema": "https://github.com/zoxy-io/zoxy/releases/download/v0.2.1/zoxy-0.2.1-config.schema.json", "listeners": [ { "bind": "127.0.0.1:8080", "cluster": "origin", "protocol": "l4" } ], "clusters": { "origin": { "endpoints": ["127.0.0.1:9000", { "address": "127.0.0.1:9001", "weight": 3 }], "check": { "type": "tcp", "fall": 3, "rise": 2 } } }, "access_log": { "sink": "stdout" }}$ zoxy config.jsonThe $schema line is what makes an editor complete and validate the file as you
type; the loader reads it as a hint and ignores it. It points at the release asset rather than at a
floating URL, so the file is checked against the format of the zoxy you are running — pin it to
whichever version that is.
Reference
Startup config for the zoxy L4/L7 proxy. Encodes structure, enums, and numeric bounds; semantic checks (canonical route prefixes/hosts, IP:port literal parsing, reserved header names, endpoint port != 0) are enforced by the loader and are not expressible in JSON Schema.
listeners[]
required at least 1 itemsSockets the proxy accepts connections on.
One accepting socket. Exactly one of `cluster` or `routes` selects the upstream — that fork is enforced by the loader, not this schema.
| required | field | type | rules | what it does |
|---|---|---|---|---|
| * | bind | string | IP:port literal to bind (hostnames are rejected — DNS is a non-goal). | |
cluster | string | Sugar for a single catch-all route to this cluster; mutually exclusive with routes. | ||
routes | object[] | at least 1 items | Explicit longest-prefix route table (http listeners only). | |
request_filters | object[] | Request filter rules, evaluated top-down (http listeners only). | ||
response_filters | object[] | Response filter rules — header edits on the origin's response, evaluated top-down (http listeners only). | ||
protocol | string | "l4" · "http"default "l4" | What the listener speaks: l4 relays bytes blindly, http runs the reverse-proxy state machine. | |
forwarded | object | Tell the origin the client's address via X-Forwarded-For (http listeners only); absent leaves the header untouched. | ||
mode | string | "replace" · "append" | replace: state the observed peer, discarding any inbound chain (use at the edge). append: extend the inbound chain with the observed peer (use only when every hop in front is trusted). | |
proxy_protocol | object | Expect a PROXY protocol header (v1 or v2) ahead of every connection's payload (l4 listeners only); absent treats first bytes as payload. | ||
mode | string | "require" | require: every connection must open with a valid v1 or v2 header; anything else is closed. Only meaningful when the listener is reachable exclusively through the fronting proxy. | |
tls | object | Terminate TLS on this listener with the given certificate and key; absent is a plaintext socket. Inbound only — the upstream leg stays plaintext. | ||
cert | string | non-empty | Path to the PEM certificate chain, leaf first. | |
key | string | non-empty | Path to the leaf's PEM private key (ECDSA P-256 or P-384). |
* required — the loader rejects a config without it.
listeners[].routes[]
at least 1 itemsOne longest-prefix route entry.
| required | field | type | rules | what it does |
|---|---|---|---|---|
| * | prefix | string | Canonical origin-form path prefix; must start with a slash. | |
| * | cluster | string | Name of the cluster matching requests route to. | |
host | string | Canonical host scope; absent matches any host. |
* required — the loader rejects a config without it.
listeners[].request_filters[]
One filter rule: a match predicate and the actions applied when it matches.
| required | field | type | rules | what it does |
|---|---|---|---|---|
| * | actions | object[] | at least 1 items | Actions applied in order when the rule matches. |
match | object | Match predicate; absent fields match anything. |
* required — the loader rejects a config without it.
listeners[].request_filters[].match
Match predicate; absent fields match anything.
| required | field | type | rules | what it does |
|---|---|---|---|---|
method | string[] | at least 1 items"GET" · "POST" · "HEAD" · "PUT" · "DELETE" · "CONNECT" · "OPTIONS" · "TRACE" · "PATCH" | Registered request-method tokens; absent matches any method. | |
host | string | Canonical host to match; absent matches any host. | ||
path_prefix | string | Canonical origin-form path prefix; must start with a slash. | ||
headers | object[] | Header predicates; all must match. | ||
client | string[] | at least 1 items | CIDR prefixes the connection's client address must fall inside (any-of): "10.0.0.0/8", up to /32 for IPv4 and /64 for IPv6 — a client's IPv6 identity is its /64, like hash source_ip. Matched against the observed peer (the PROXY-announced client on a proxy_protocol listener), never an X-Forwarded-For chain. |
listeners[].request_filters[].match.headers[]
One header predicate. Exactly one of `present`/`equals`/`contains` selects the kind — that fork is enforced by the loader.
| required | field | type | rules | what it does |
|---|---|---|---|---|
| * | name | string | Header field name (matched case-insensitively). | |
present | true | always true | Matches when the header is present (present: false is rejected). | |
equals | string | Matches when the header value equals this string. | ||
contains | string | non-empty | Matches when the header value contains this substring. |
* required — the loader rejects a config without it.
listeners[].request_filters[].actions[]
at least 1 itemsOne filter action. Exactly one field is set — the action's kind — and that fork is enforced by the loader.
| required | field | type | rules | what it does |
|---|---|---|---|---|
reject | integer | 400 · 403 · 404 · 429 | Reject the request with this status code. | |
redirect | object | Answer the request with a redirect instead of forwarding it. | ||
status | integer | 301 · 302 · 307 · 308default 301 | Redirect status: permanent/temporary x method-preserving. | |
location | string | non-empty | Fixed Location value, sent verbatim; mutually exclusive with scheme/host. | |
scheme | string | "http" · "https" | Replacement scheme for a composed target. Required there — never guessed, since behind a TLS terminator every hop this proxy sees is plaintext. | |
host | string | Replacement canonical host for a composed target; absent keeps the request's own. | ||
respond | object | Answer the request from a configured body instead of forwarding it — this proxy as the origin. | ||
status | integer | 200 · 400 · 403 · 404 · 413 · 414 · 429 · 431 · 501 · 502 · 503 · 504default 200 | Status to answer with: 200, or one of the error statuses this proxy sends. | |
body | string | non-empty | Name of the body to serve, from the top-level `bodies` map. | |
header_set | object | Set (replace) a request header. | ||
name | string | Header field name. | ||
value | string | Header field value. | ||
header_add | object | Append a request header. | ||
name | string | Header field name. | ||
value | string | Header field value. | ||
header_remove | string | Remove a request header by name. | ||
rewrite_prefix | object | Rewrite the request path prefix before proxying. | ||
from | string | Canonical path prefix to strip; must start with a slash. | ||
to | string | Canonical path prefix to prepend; must start with a slash. |
listeners[].response_filters[]
One response filter rule: a match over the origin's response and the header edits applied when it matches.
| required | field | type | rules | what it does |
|---|---|---|---|---|
| * | actions | object[] | at least 1 items | Header edits (header_set / header_add / header_remove) applied in order when the rule matches; reject and rewrite_prefix are request-side only. |
match | object | Response-match predicate; absent fields match anything. |
* required — the loader rejects a config without it.
listeners[].response_filters[].match
Response-match predicate; absent fields match anything.
| required | field | type | rules | what it does |
|---|---|---|---|---|
status | integer[] | at least 1 items | Exact response status codes to match; absent matches any status. | |
status_class | string | "1xx" · "2xx" · "3xx" · "4xx" · "5xx" | Response status class to match ("1xx" through "5xx"); absent matches any class. | |
headers | object[] | Response-header predicates; all must match. |
listeners[].response_filters[].match.headers[]
One header predicate. Exactly one of `present`/`equals`/`contains` selects the kind — that fork is enforced by the loader.
| required | field | type | rules | what it does |
|---|---|---|---|---|
| * | name | string | Header field name (matched case-insensitively). | |
present | true | always true | Matches when the header is present (present: false is rejected). | |
equals | string | Matches when the header value equals this string. | ||
contains | string | non-empty | Matches when the header value contains this substring. |
* required — the loader rejects a config without it.
listeners[].response_filters[].actions[]
at least 1 itemsOne filter action. Exactly one field is set — the action's kind — and that fork is enforced by the loader.
| required | field | type | rules | what it does |
|---|---|---|---|---|
reject | integer | 400 · 403 · 404 · 429 | Reject the request with this status code. | |
redirect | object | Answer the request with a redirect instead of forwarding it. | ||
status | integer | 301 · 302 · 307 · 308default 301 | Redirect status: permanent/temporary x method-preserving. | |
location | string | non-empty | Fixed Location value, sent verbatim; mutually exclusive with scheme/host. | |
scheme | string | "http" · "https" | Replacement scheme for a composed target. Required there — never guessed, since behind a TLS terminator every hop this proxy sees is plaintext. | |
host | string | Replacement canonical host for a composed target; absent keeps the request's own. | ||
respond | object | Answer the request from a configured body instead of forwarding it — this proxy as the origin. | ||
status | integer | 200 · 400 · 403 · 404 · 413 · 414 · 429 · 431 · 501 · 502 · 503 · 504default 200 | Status to answer with: 200, or one of the error statuses this proxy sends. | |
body | string | non-empty | Name of the body to serve, from the top-level `bodies` map. | |
header_set | object | Set (replace) a request header. | ||
name | string | Header field name. | ||
value | string | Header field value. | ||
header_add | object | Append a request header. | ||
name | string | Header field name. | ||
value | string | Header field value. | ||
header_remove | string | Remove a request header by name. | ||
rewrite_prefix | object | Rewrite the request path prefix before proxying. | ||
from | string | Canonical path prefix to strip; must start with a slash. | ||
to | string | Canonical path prefix to prepend; must start with a slash. |
clusters{}
required at least 1 entriesNamed upstream clusters, keyed by cluster name.
One upstream cluster: its endpoints, pick policy and health checks.
| required | field | type | rules | what it does |
|---|---|---|---|---|
| * | endpoints | (string | object)[] | at least 1 items | Endpoints: IP:port literals (port must be non-zero), each optionally an object adding a pick weight (#174). |
pick | string | object | "p2c" · "rr" | Endpoint-pick policy: p2c (power-of-two-choices) or rr (strict round-robin) as a bare string, or an object choosing hash (stickiness) and naming its key. | |
policy | string | "rr" · "p2c" · "hash" | Pick policy: p2c (power-of-two-choices), rr (strict round-robin), or hash (stickiness on an explicit key). | |
key | string | "source_ip" · "header" · "cookie" | What a hash cluster is sticky on: source_ip (all four bytes of an IPv4 address, the /64 prefix of an IPv6 one — RFC 8981 rotation survives), header (rendezvous on the named header's value), or cookie (the named cookie carries the endpoint assignment itself, minted by zoxy — #178). | |
name | string | non-empty | The header or cookie name a request-derived key reads; required for those keys, rejected for source_ip. | |
check | object | Active health checks for every endpoint in this cluster; absent leaves them off. | ||
type | string | "tcp" · "http"default "tcp" | What a probe proves: tcp (the port accepts) or http (a path answered the expected status). | |
fall | integer | default 31–64 | Consecutive failed probes that eject an endpoint from balancing. | |
rise | integer | default 21–64 | Consecutive successful probes that restore an ejected endpoint. | |
timeout_ms | integer | 1–3600000 | Budget for one whole probe; absent uses timeouts.connect_ms. | |
path | string | non-empty | Canonical origin-form path an http probe requests; required for http, rejected for tcp. | |
host | string | non-empty | Host header an http probe sends; absent sends the endpoint's own IP:port literal. | |
expect_status | integer | default 200100–599 | The one response status an http probe accepts as healthy. | |
max_inflight | integer | 1–22932 | Cap on concurrent in-flight work per endpoint; absent leaves the cluster uncapped. | |
proxy_protocol | object | Announce each client to this cluster's origins with a PROXY protocol header on the upstream connection (l4-reachable clusters only); absent sends none. | ||
send | string | "v1" · "v2" | PROXY protocol version to write: v1 (text) or v2 (binary — what cloud load balancers speak). |
* required — the loader rejects a config without it.
clusters{}.endpoints[]
at least 1 itemsOne endpoint: an IP:port literal, or an object carrying the literal and a relative pick weight.
| required | field | type | rules | what it does |
|---|---|---|---|---|
| * | address | string | non-empty | IP:port endpoint literal (port must be non-zero). |
weight | integer | default 10–256 | Relative share of the cluster's traffic under every pick policy; endpoints default to equal (1). 0 drains the endpoint: still health-checked, never picked. |
* required — the loader rejects a config without it.
timeouts
optional Connection lifecycle deadlines (milliseconds).
| required | field | type | rules | what it does |
|---|---|---|---|---|
connect_ms | integer | default 50001–3600000 | Per-try upstream connect budget. | |
idle_ms | integer | default 600001–3600000 | Idle / head-read deadline; must exceed connect_ms. | |
drain_deadline_ms | integer | default 00–3600000 | Graceful-drain deadline on shutdown; 0 waits indefinitely. | |
max_lifetime_ms | integer | default 00–3600000 | Absolute connection-age cap; 0 disables it. | |
request_ms | integer | default 00–3600000 | Cap on one L7 exchange, not refreshed by activity; 0 disables it. | |
health_interval_ms | integer | default 20001–3600000 | Pause between health-probe sweeps over checked clusters. |
limits
optional Optional pool sizes and the CQ-fill headroom knob; absent fields take the lean defaults.
| required | field | type | rules | what it does |
|---|---|---|---|---|
conn_slots | integer | 1–11466 | Concurrent connection slots. | |
relay_buffers | integer | 1–11466 | Relay buffer pairs (bounds concurrent L4 and L7 body relays). | |
upstream_slots | integer | 1–11466 | Shared upstream connection slots. | |
head_buffers | integer | 0–11466 | HTTP head buffers in the shared ring (bounds request heads in flight; idle keep-alive connections hold none). Defaults to conn_slots; must be 1..conn_slots with an http listener, 0 without one. | |
upstream_head_buffers | integer | 0–11466 | Upstream head buffers (bounds exchanges in their head phase; parked origin connections hold none). Defaults to upstream_slots; must be 1..upstream_slots with an http listener, 0 without one. | |
head_buffer_bytes | integer | 1024–1048576 | Bytes per head buffer in both head pools — the largest HTTP head accepted (oversize requests get 414/431). May be raised for big-cookie/JWT traffic as well as lowered. | |
tls_engines | integer | 1–1024 | Concurrent TLS sessions — handshaking or terminated. The largest per-connection object zoxy holds (~132 KiB plus a plaintext buffer), so this is what a TLS deployment's memory follows. Zero exactly when no listener terminates TLS. | |
cq_fill_eighths | integer | 1–7 | Eighths of the io_uring completion queue the worst-case in-flight ops may fill; lower reserves more burst headroom but lowers the feasible connection-slot ceiling. | |
access_log_buffer_bytes | integer | 3686–1048576 | Bytes per access-log staging buffer, of which there are two; larger tolerates a slower sink before lines are dropped. Only valid alongside an `access_log` block. |
admin
optional Optional admin/metrics listener; absent leaves it off.
| required | field | type | rules | what it does |
|---|---|---|---|---|
| * | bind | string | IP:port literal to bind the admin/metrics listener (hostnames are rejected). |
* required — the loader rejects a config without it.
access_log
optional Optional per-request/per-connection JSON access log; absent leaves it off.
| required | field | type | rules | what it does |
|---|---|---|---|---|
| * | sink | string | "stdout" · "file" | Where lines are written. `stdout` is the process's own standard output; `file` appends to `path`. |
path | string | File the `file` sink appends to: created if absent, opened append-only at startup (one extra fd), never truncated — so a copy-truncate rotation is safe. Required by `sink:"file"`, rejected beside `stdout`, which cannot use it. | ||
request_headers | string[] | up to 8 items | Request headers to record under `request_headers` on each http line — an upstream's X-Request-ID or traceparent is what joins this log to the origin's. Names are matched case-insensitively and logged lowercased; absent headers are omitted from the line. | |
response_headers | string[] | up to 8 items | Response headers to record under `response_headers`, on the same terms — the origin's X-Cache, say. Ignored for l4 lines, which have no response to read. |
* required — the loader rejects a config without it.
bodies{}
optional Named response bodies (file or inline, read once at startup), referenced by name from error_pages.
One named response body: bytes from a file read at startup, or an inline string — exactly one of the two — plus the Content-Type it is served with. Referenced by name from error_pages (and any future body-serving feature), so one body is one buffer however many places serve it.
| required | field | type | rules | what it does |
|---|---|---|---|---|
| * | content_type | string | non-empty | The Content-Type header value this body is served with; nothing is inferred from a filename. |
file | string | Path to the body's file, read once at startup; a change needs a restart (parse-once config). | ||
inline | string | The body's bytes, verbatim, for content small enough to live in the config. |
* required — the loader rejects a config without it.
error_pages{}
optional Bodies for the statuses zoxy sends itself, keyed by status literal, each naming a body. Configuring a status is the opt-in — absent, every static response stays empty, sheds included.
| required | field | type | rules | what it does |
|---|---|---|---|---|
<key> | string | Name of a configured body. |