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.

config.json
{    "$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.json

The $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 items

Sockets 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.

requiredfieldtyperuleswhat it does
*bindstringIP:port literal to bind (hostnames are rejected — DNS is a non-goal).
clusterstringSugar for a single catch-all route to this cluster; mutually exclusive with routes.
routesobject[]at least 1 itemsExplicit longest-prefix route table (http listeners only).
request_filtersobject[]Request filter rules, evaluated top-down (http listeners only).
response_filtersobject[]Response filter rules — header edits on the origin's response, evaluated top-down (http listeners only).
protocolstring"l4" · "http"default "l4"What the listener speaks: l4 relays bytes blindly, http runs the reverse-proxy state machine.
forwardedobjectTell the origin the client's address via X-Forwarded-For (http listeners only); absent leaves the header untouched.
modestring"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_protocolobjectExpect a PROXY protocol header (v1 or v2) ahead of every connection's payload (l4 listeners only); absent treats first bytes as payload.
modestring"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.
tlsobjectTerminate TLS on this listener with the given certificate and key; absent is a plaintext socket. Inbound only — the upstream leg stays plaintext.
certstringnon-emptyPath to the PEM certificate chain, leaf first.
keystringnon-emptyPath 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 items

One longest-prefix route entry.

requiredfieldtyperuleswhat it does
*prefixstringCanonical origin-form path prefix; must start with a slash.
*clusterstringName of the cluster matching requests route to.
hoststringCanonical 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.

requiredfieldtyperuleswhat it does
*actionsobject[]at least 1 itemsActions applied in order when the rule matches.
matchobjectMatch predicate; absent fields match anything.

* required — the loader rejects a config without it.

listeners[].request_filters[].match

Match predicate; absent fields match anything.

requiredfieldtyperuleswhat it does
methodstring[]at least 1 items"GET" · "POST" · "HEAD" · "PUT" · "DELETE" · "CONNECT" · "OPTIONS" · "TRACE" · "PATCH"Registered request-method tokens; absent matches any method.
hoststringCanonical host to match; absent matches any host.
path_prefixstringCanonical origin-form path prefix; must start with a slash.
headersobject[]Header predicates; all must match.
clientstring[]at least 1 itemsCIDR 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.

requiredfieldtyperuleswhat it does
*namestringHeader field name (matched case-insensitively).
presenttruealways trueMatches when the header is present (present: false is rejected).
equalsstringMatches when the header value equals this string.
containsstringnon-emptyMatches when the header value contains this substring.

* required — the loader rejects a config without it.

listeners[].request_filters[].actions[]

at least 1 items

One filter action. Exactly one field is set — the action's kind — and that fork is enforced by the loader.

requiredfieldtyperuleswhat it does
rejectinteger400 · 403 · 404 · 429Reject the request with this status code.
redirectobjectAnswer the request with a redirect instead of forwarding it.
statusinteger301 · 302 · 307 · 308default 301Redirect status: permanent/temporary x method-preserving.
locationstringnon-emptyFixed Location value, sent verbatim; mutually exclusive with scheme/host.
schemestring"http" · "https"Replacement scheme for a composed target. Required there — never guessed, since behind a TLS terminator every hop this proxy sees is plaintext.
hoststringReplacement canonical host for a composed target; absent keeps the request's own.
respondobjectAnswer the request from a configured body instead of forwarding it — this proxy as the origin.
statusinteger200 · 400 · 403 · 404 · 413 · 414 · 429 · 431 · 501 · 502 · 503 · 504default 200Status to answer with: 200, or one of the error statuses this proxy sends.
bodystringnon-emptyName of the body to serve, from the top-level `bodies` map.
header_setobjectSet (replace) a request header.
namestringHeader field name.
valuestringHeader field value.
header_addobjectAppend a request header.
namestringHeader field name.
valuestringHeader field value.
header_removestringRemove a request header by name.
rewrite_prefixobjectRewrite the request path prefix before proxying.
fromstringCanonical path prefix to strip; must start with a slash.
tostringCanonical 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.

requiredfieldtyperuleswhat it does
*actionsobject[]at least 1 itemsHeader edits (header_set / header_add / header_remove) applied in order when the rule matches; reject and rewrite_prefix are request-side only.
matchobjectResponse-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.

requiredfieldtyperuleswhat it does
statusinteger[]at least 1 itemsExact response status codes to match; absent matches any status.
status_classstring"1xx" · "2xx" · "3xx" · "4xx" · "5xx"Response status class to match ("1xx" through "5xx"); absent matches any class.
headersobject[]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.

requiredfieldtyperuleswhat it does
*namestringHeader field name (matched case-insensitively).
presenttruealways trueMatches when the header is present (present: false is rejected).
equalsstringMatches when the header value equals this string.
containsstringnon-emptyMatches when the header value contains this substring.

* required — the loader rejects a config without it.

listeners[].response_filters[].actions[]

at least 1 items

One filter action. Exactly one field is set — the action's kind — and that fork is enforced by the loader.

requiredfieldtyperuleswhat it does
rejectinteger400 · 403 · 404 · 429Reject the request with this status code.
redirectobjectAnswer the request with a redirect instead of forwarding it.
statusinteger301 · 302 · 307 · 308default 301Redirect status: permanent/temporary x method-preserving.
locationstringnon-emptyFixed Location value, sent verbatim; mutually exclusive with scheme/host.
schemestring"http" · "https"Replacement scheme for a composed target. Required there — never guessed, since behind a TLS terminator every hop this proxy sees is plaintext.
hoststringReplacement canonical host for a composed target; absent keeps the request's own.
respondobjectAnswer the request from a configured body instead of forwarding it — this proxy as the origin.
statusinteger200 · 400 · 403 · 404 · 413 · 414 · 429 · 431 · 501 · 502 · 503 · 504default 200Status to answer with: 200, or one of the error statuses this proxy sends.
bodystringnon-emptyName of the body to serve, from the top-level `bodies` map.
header_setobjectSet (replace) a request header.
namestringHeader field name.
valuestringHeader field value.
header_addobjectAppend a request header.
namestringHeader field name.
valuestringHeader field value.
header_removestringRemove a request header by name.
rewrite_prefixobjectRewrite the request path prefix before proxying.
fromstringCanonical path prefix to strip; must start with a slash.
tostringCanonical path prefix to prepend; must start with a slash.

clusters{}

required at least 1 entries

Named upstream clusters, keyed by cluster name.

One upstream cluster: its endpoints, pick policy and health checks.

requiredfieldtyperuleswhat it does
*endpoints(string | object)[]at least 1 itemsEndpoints: IP:port literals (port must be non-zero), each optionally an object adding a pick weight (#174).
pickstring | 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.
policystring"rr" · "p2c" · "hash"Pick policy: p2c (power-of-two-choices), rr (strict round-robin), or hash (stickiness on an explicit key).
keystring"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).
namestringnon-emptyThe header or cookie name a request-derived key reads; required for those keys, rejected for source_ip.
checkobjectActive health checks for every endpoint in this cluster; absent leaves them off.
typestring"tcp" · "http"default "tcp"What a probe proves: tcp (the port accepts) or http (a path answered the expected status).
fallintegerdefault 31–64Consecutive failed probes that eject an endpoint from balancing.
riseintegerdefault 21–64Consecutive successful probes that restore an ejected endpoint.
timeout_msinteger1–3600000Budget for one whole probe; absent uses timeouts.connect_ms.
pathstringnon-emptyCanonical origin-form path an http probe requests; required for http, rejected for tcp.
hoststringnon-emptyHost header an http probe sends; absent sends the endpoint's own IP:port literal.
expect_statusintegerdefault 200100–599The one response status an http probe accepts as healthy.
max_inflightinteger1–22932Cap on concurrent in-flight work per endpoint; absent leaves the cluster uncapped.
proxy_protocolobjectAnnounce each client to this cluster's origins with a PROXY protocol header on the upstream connection (l4-reachable clusters only); absent sends none.
sendstring"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 items

One endpoint: an IP:port literal, or an object carrying the literal and a relative pick weight.

requiredfieldtyperuleswhat it does
*addressstringnon-emptyIP:port endpoint literal (port must be non-zero).
weightintegerdefault 10–256Relative 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).

requiredfieldtyperuleswhat it does
connect_msintegerdefault 50001–3600000Per-try upstream connect budget.
idle_msintegerdefault 600001–3600000Idle / head-read deadline; must exceed connect_ms.
drain_deadline_msintegerdefault 00–3600000Graceful-drain deadline on shutdown; 0 waits indefinitely.
max_lifetime_msintegerdefault 00–3600000Absolute connection-age cap; 0 disables it.
request_msintegerdefault 00–3600000Cap on one L7 exchange, not refreshed by activity; 0 disables it.
health_interval_msintegerdefault 20001–3600000Pause between health-probe sweeps over checked clusters.

limits

optional

Optional pool sizes and the CQ-fill headroom knob; absent fields take the lean defaults.

requiredfieldtyperuleswhat it does
conn_slotsinteger1–11466Concurrent connection slots.
relay_buffersinteger1–11466Relay buffer pairs (bounds concurrent L4 and L7 body relays).
upstream_slotsinteger1–11466Shared upstream connection slots.
head_buffersinteger0–11466HTTP 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_buffersinteger0–11466Upstream 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_bytesinteger1024–1048576Bytes 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_enginesinteger1–1024Concurrent 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_eighthsinteger1–7Eighths 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_bytesinteger3686–1048576Bytes 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.

requiredfieldtyperuleswhat it does
*bindstringIP: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.

requiredfieldtyperuleswhat it does
*sinkstring"stdout" · "file"Where lines are written. `stdout` is the process's own standard output; `file` appends to `path`.
pathstringFile 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_headersstring[]up to 8 itemsRequest 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_headersstring[]up to 8 itemsResponse 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.

requiredfieldtyperuleswhat it does
*content_typestringnon-emptyThe Content-Type header value this body is served with; nothing is inferred from a filename.
filestringPath to the body's file, read once at startup; a change needs a restart (parse-once config).
inlinestringThe 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.

requiredfieldtyperuleswhat it does
<key>stringName of a configured body.