Skip to main content

trillium gateway

Where serve and proxy each do one thing configured from flags, gateway reads a KDL config file and assembles the same building blocks — static files, reverse proxy, redirects, header and HTML rewriting, compression, caching, rate limiting, TLS / h3 — into one or more listeners. It's a trillium-backed caddy / nginx-lite.

Feature-gated

gateway is not in the default build. Install it with cargo install trillium-cli --features gateway, or run from a checkout with cargo run --features gateway -- gateway. The gateway feature implies rustls; with the default h3 feature it also serves HTTP/3 over QUIC.

Running

trillium gateway --config gateway.kdl
trillium gateway --config gateway.kdl --check # parse + print the resolved config, don't serve
FlagEnvDefaultNotes
-c, --configTRILLIUM_GATEWAY_CONFIGgateway.kdlpath to the KDL config file
--checkparse, validate, print the config; exit

--check parses the file, validates it (including every rewrite-html CSS selector), and prints the resolved configuration without binding any sockets — use it in CI or before a reload. Config errors are reported with miette source spans pointing at the offending line.

On startup, gateway prints a colored summary of every binding and the routes it serves, so you can see at a glance what each listener does.

Anatomy of a config

A config has optional cross-cutting defaults at the top, then one or more binding blocks. Each binding is a listener; within it, ordered route patterns dispatch by path to a stack of directives.

compression true
rate-limit "100/min" burst=200

binding ":443" {
tls cert="./cert.pem" key="./key.pem"
http {
received-body-max-len "10MiB"
}

route "/api/*" {
proxy strategy="round-robin" {
upstream "http://127.0.0.1:9000"
upstream "http://127.0.0.1:9001"
}
}

route "/old/*" {
redirect "https://example.com/new" status=308
}

route "/*" {
headers {
add "X-Served-By" "trillium"
remove "Server"
}
files root="./public" index="index.html" directory-listing=true
}
}

The pieces:

  • Routing & directivesroute patterns and the files / proxy / redirect / headers directives that make up a route's handler stack.
  • HTML rewriting — the rewrite-html directive, a declarative streaming HTML transformer.
  • Virtual hostshost blocks that dispatch by Host header on a shared socket, with per-host SNI certificates.
KDL child blocks need a line break

The KDL parser rejects a child block written entirely on one line: proxy { upstream "..." } is a parse error. Put the child on its own line (or end it with a ;). Every example here uses the multiline form.

Bindings

A binding is one listener: a host:port address plus optional TLS and per-binding HTTP tuning. Declare several to run multiple listeners in one process.

binding "0.0.0.0:8080" {
route "/*" {
files root="./public"
}
}

binding ":443" {
tls cert="./cert.pem" key="./key.pem"
route "/*" {
files root="./public"
}
}

The listen address is host:port. A bare :443 (empty host) binds all interfaces — the nginx listen :80 convention. With the h3 feature, a TLS binding also speaks HTTP/3 over QUIC on the same port.

Per-binding HTTP tuning

An http { … } block overrides trillium_http::HttpConfig defaults for that listener. Only the keys you set are changed; size-valued keys accept human units ("10MiB").

binding ":8080" {
http {
received-body-max-len "10MiB"
head-max-len "64KiB"
max-connections 10000
}
route "/*" {
files root="./public"
}
}

Cross-cutting defaults

Three nodes at the top of the document configure behavior inherited by every binding.

compression

compression true // default; set `compression false` to disable everywhere

Compression (gzip / brotli / zstd, by Accept-Encoding) is on by default. compression false turns it off across all bindings.

rate-limit

rate-limit "100/min" burst=200

A per-client-network rate limit applied to every binding. The rate is written COUNT/WINDOW (window s, min, or h); burst permits short spikes above the sustained rate and defaults to the rate count. Over-quota requests get 429 Too Many Requests with a Retry-After header, and metered responses carry the standard RateLimit / RateLimit-Policy headers. (Same engine as serve and proxy.)

cache

A response cache for proxy directives. Opt-in — absent means no caching. (This is the opposite of trillium proxy, where caching is on by default; a gateway shouldn't silently cache dynamic upstreams.) A bare cache node enables it with defaults; the children tune it.

cache {
capacity "256MiB" // total in-memory size (default 256MiB)
max-body "16MiB" // largest cacheable body; bigger streams uncached (default 16MiB)
time-to-idle "5m" // evict entries not read within this duration
time-to-live "1h" // evict entries this long after they're stored
}

One cache (and one connection pool) is shared across every proxy directive in the whole process. When caching is enabled, the gateway also adds ETag / Cache-Control handling to its own responses.

Graceful shutdown

All bindings share a single shutdown signal: one Ctrl-C (or SIGINT, SIGTERM, SIGQUIT on Unix) drains every listener gracefully, letting in-flight requests finish before the process exits.