Skip to content

configuration

github-actions[bot] edited this page Jun 12, 2026 · 1 revision

Configuration Reference

Conduit is configured with a single file in JSON or YAML format. Both are fully equivalent — YAML is recommended because it supports comments.

conduit -c conduit.yaml          # explicit path
conduit -c conduit.json          # JSON is fine too
conduit                          # auto-discover: conduit.json → conduit.yaml → conduit.yml
conduit validate -c conduit.yaml # validate without starting

Fields accept environment variable references"$MY_VAR" is replaced at startup. This keeps secrets out of config files.

Optional build features

The binaries and Docker images published as "standard" already include the standard feature bundle (jwt, consumers, forward-auth, cache, acme — see the standard row below). Some config sections still require an additional feature flag (or --features full):

Feature Flag Config section
jwt --features jwt jwtAuth + {{ jwt.* }} templates
consumers --features consumers consumers
forward-auth --features forward-auth forwardAuth
rhai --features rhai middleware[].type: "script"
wasm --features wasm middleware[].type: "wasm"
tcp --features tcp type: "tcp" site
upload --features upload upload
redis --features redis rateLimit.store: "redis://...", cache.store: "redis://..."
cache --features cache proxy.*.cache
disk-cache --features disk-cache cache.store: "disk:/path"
acme --features acme tls.acme
fault-injection --features fault-injection faultInjection
otlp --features otlp global.otlp
tokio-metrics --features tokio-metrics conduit_eventloop_lag_ms Prometheus gauge (no config key)
kubernetes --features kubernetes --kubernetes-namespace CLI flag (not a config field)
standard --features standard Bundle: jwt + consumers + forward-auth + cache + acme
full --features full All of the above

Download a -full binary from GitHub Releases or build from source: cargo build --release --features full. See docs/cli.md — Build features for binary sizes and details.


Table of Contents

Background

  • Concepts — request pipeline, forwarded headers, skipPaths glob

Essentials

Routing

Reliability

Caching

Authentication

Rate Limiting & Load Shedding

Transforms

Observability

Security

Middleware

Advanced


Concepts

These sections explain how Conduit behaves — not config fields you set, but background knowledge that helps understand the rest of the reference.

Request pipeline

The order in which configured features are applied for every incoming request:

Incoming request
  │
  ├─ 1. X-Request-ID injection        — auto-generate UUID v4 if absent
  ├─ 2. IP filter                     — 403 if blocked (before any auth)
  ├─ 3. CORS preflight                — OPTIONS short-circuit
  ├─ 4. Health / ACME bypass          — skip all guards for /__health__ etc.
  ├─ 5. Inflight limit                — 503 if maxInflightRequests exceeded
  ├─ 6. Site-level rate limit         — 429 if over limit
  ├─ 7. Consumers                     — identify named client (V1/V2/V3)
  ├─ 8. Basic Auth                    — 401 if credentials missing/wrong
  ├─ 9. API Key                       — 401 if key missing/wrong
  ├─ 10. JWT Auth                     — 401 if token invalid
  ├─ 11. Forward Auth                 — delegate to external service
  ├─ 12. Redirects                    — 301/308 if path matches
  ├─ 13. Fault injection              — abort/delay (testing only)
  ├─ 14. Rhai / WASM middleware       — custom scripts in order
  │
  ├─ Route matching + per-route rate limit
  ├─ Circuit breaker check
  │
  └─ Upstream request
       ├─ X-Forwarded-For / -Proto / -Host injection
       ├─ requestTransform (setHeaders / removeHeaders)
       ├─ Path rewrite (stripPrefix / rewrite rules)
       └─ Traffic mirror (fire-and-forget)

Upstream response
  ├─ CRLF header protection
  ├─ CORS / security / custom headers injection
  ├─ responseTransform
  ├─ X-Response-Time header
  ├─ Retry-on-error decision
  └─ Error masking (5xx body replacement)

Steps 7–11 are mutually exclusive in practice — only one auth method identifies the client, but multiple may be configured as fallbacks. The consumers guard runs before basicAuth and apiKey, so a consumer match takes priority.

Forwarded headers

Conduit automatically injects these headers into every proxied upstream request. No configuration is needed — they are always present.

Header Value Notes
X-Forwarded-For Client IP Appended to existing value if already present
X-Forwarded-Proto http or https Derived from whether the site has TLS configured
X-Forwarded-Host Original Host header Lets upstreams reconstruct full URLs
X-Request-ID UUID v4 Auto-generated if absent; forwarded as-is if client sends it
Via 1.1 conduit RFC 7230 §5.7 — identifies the proxy hop; appended to existing value

To remove any of these before forwarding, use requestTransform.removeHeaders:

# conduit.yaml
requestTransform:
  removeHeaders: [X-Forwarded-For, X-Forwarded-Host]
// conduit.json
{
  "requestTransform": {
    "removeHeaders": ["X-Forwarded-For", "X-Forwarded-Host"]
  }
}

skipPaths glob syntax

Many config sections accept a skipPaths list — requests whose path matches are bypassed by that feature entirely. It is not a top-level field; it appears inside basicAuth, apiKey, jwtAuth, forwardAuth, consumers, rateLimit, and logging.

Two pattern forms are supported:

Pattern Matches
/exact/path Only that exact path
/prefix/** The prefix itself, /prefix/, and any sub-path
# conduit.yaml — example inside jwtAuth
jwtAuth:
  secret: "$JWT_SECRET"
  skipPaths:
    - /__health__ # exact match — health check bypasses JWT
    - /public/** # /public, /public/, /public/assets/logo.png, …
// conduit.json
{
  "jwtAuth": {
    "secret": "$JWT_SECRET",
    "skipPaths": ["/__health__", "/public/**"]
  }
}

Note: only /** at the end is supported as a wildcard. Patterns like /api/*/details or /**.json are treated as exact matches (no mid-path or extension wildcards).


Port and Host

# YAML
port: 8080
host: api.example.com # optional — virtual hosting
// JSON
{ "port": 8080, "host": "api.example.com" }
Field Type Default Description
port number 80 / 443 ¹ TCP port to listen on
host string Virtual hostname. Omit to match all Host headers (catch-all)

¹ Default port is 443 when tls is configured, 80 otherwise. When no sites are configured at all, Conduit listens on 8080 as a fallback.

Use host when running multiple sites on the same process.


TLS / HTTPS

Manual certificates

# YAML
port: 443
tls:
  cert: /etc/tls/server.crt
  key: /etc/tls/server.key
  httpRedirectPort: 80
  versions: ["TLSv1.2", "TLSv1.3"]
// JSON
{
  "port": 443,
  "tls": {
    "cert": "/etc/tls/server.crt",
    "key": "/etc/tls/server.key",
    "httpRedirectPort": 80,
    "versions": ["TLSv1.2", "TLSv1.3"]
  }
}

Auto-TLS via Let's Encrypt

Requires cargo build --features acme

# YAML
port: 443
tls:
  acme:
    email: admin@example.com
    storage: ./certs
    challenge: http-01
// JSON
{
  "port": 443,
  "tls": {
    "acme": {
      "email": "admin@example.com",
      "storage": "./certs",
      "challenge": "http-01"
    }
  }
}

TLS field reference

Field Type Default Description
cert path PEM certificate file
key path PEM private key file
ca path CA bundle for upstream verification
httpRedirectPort number Port that redirects HTTP to HTTPS
versions string[] all Allowed TLS versions — rustls format ("TLSv1.2", "TLSv1.3")
ciphers string[] all Allowed cipher suites — rustls names, not OpenSSL
acme.email string Contact email for ACME account
acme.storage path Directory for certificate persistence
acme.challenge string "http-01" or "dns-01"
acme.directory string Custom ACME directory URL. Use "https://acme-staging-v02.api.letsencrypt.org/directory" for Let's Encrypt staging (rate-limit-free testing)
clientAuth object mTLS client cert verification

Note — single cert per port: rustls does not support per-SNI certificate selection. When multiple HTTPS sites share the same port, the first registered cert is used for all. Use separate ports for different certificates.


HTTP/2

# YAML
port: 443
tls:
  cert: ./certs/cert.pem
  key: ./certs/key.pem
http2: {} # enable HTTP/2 with defaults
// JSON
{
  "port": 443,
  "tls": { "cert": "./certs/cert.pem", "key": "./certs/key.pem" },
  "http2": {}
}

Full config with all fields:

http2:
  maxConcurrentStreams: 100
  initialWindowSize: 65535
  h2c: false   # HTTP/2 cleartext (internal gRPC without TLS)
// JSON
{
  "http2": {
    "maxConcurrentStreams": 100,
    "initialWindowSize": 65535,
    "h2c": false
  }
}
Field Type Default Description
maxConcurrentStreams number 100 Max parallel streams per connection
initialWindowSize number 65535 Flow-control window in bytes
h2c bool false Allow HTTP/2 upgrade on plaintext connections (h2c). For TLS ports HTTP/2 is negotiated via ALPN regardless. Useful for internal gRPC without TLS.

Compression

Add Content-Encoding: br / zstd / gzip / deflate to responses.

# YAML — shorthand (enable with defaults)
compression: true

# YAML — fine-grained
compression:
  algorithms: [br, zstd, gzip]  # Brotli first, then Zstd, then gzip
  level: 6                       # 1 = fastest, 9 = smallest
  minBytes: 1024                 # skip responses smaller than 1 KB
// JSON — shorthand
{ "compression": true }

// JSON — fine-grained
{
  "compression": {
    "algorithms": ["br", "zstd", "gzip"],
    "level": 6,
    "minBytes": 1024
  }
}

The types field filters which response Content-Types are compressed:

compression:
  algorithms: [br, gzip]
  types:
    - "text/"
    - "application/json"
    - "application/xml"
    - "application/javascript"
    - "image/svg"
Field Type Default Description
algorithms string[] ["br", "zstd", "gzip"] Compression algorithms to offer. Supported: "br" (Brotli), "zstd" (Zstandard), "gzip", "deflate"
level number 6 Compression level (1–9)
minBytes number 1024 Minimum response size to compress (bytes)
types string[] ["text/", "application/json", "application/xml", "application/javascript", "application/xhtml", "image/svg"] Content-Type prefixes to compress. Use ["*"] to compress all types (not recommended for binary content)

Response Time Header

Inject X-Response-Time: <ms> into every response.

# YAML
responseTime: true

# With custom precision
responseTime:
  digits: 3   # decimal places in the millisecond value
// JSON
{ "responseTime": true }
// JSON
{ "responseTime": { "digits": 3 } }

Proxy

The proxy object maps URL path prefixes to upstream targets.

Single upstream (shorthand)

# YAML
proxy:
  /api: "http://backend:4000"
// JSON
{ "proxy": { "/api": "http://backend:4000" } }

Multiple targets

# YAML
proxy:
  /api:
    targets:
      - "http://backend-1:4000"
      - "http://backend-2:4000"
    strategy: round-robin
    stripPrefix: true
// JSON
{
  "proxy": {
    "/api": {
      "targets": ["http://backend-1:4000", "http://backend-2:4000"],
      "strategy": "round-robin",
      "stripPrefix": true
    }
  }
}

URL rewriting

Rewrite rules are evaluated in order. The first matching rule is applied.

# YAML
proxy:
  /api:
    targets: ["http://backend:4000"]
    stripPrefix: true
    rewrite:
      - from: "^/v[0-9]+/(.+)$" # strip version prefix
        to: "/$1"
      - from: "^/users/([0-9]+)$" # migrate legacy paths
        to: "/members/$1"
// JSON
{
  "proxy": {
    "/api": {
      "targets": ["http://backend:4000"],
      "stripPrefix": true,
      "rewrite": [
        { "from": "^/v[0-9]+/(.+)$", "to": "/$1" },
        { "from": "^/users/([0-9]+)$", "to": "/members/$1" }
      ]
    }
  }
}

Proxy route field reference

Field Type Default Description
targets string[] or object[] Upstream URLs (plain strings or {url, weight})
strategy string round-robin Load balancing — see Load balancing
stripPrefix bool false Remove matched path prefix before forwarding
hashKey string Key for hash-based strategies: "ip", "url", "header:X-Name"
rewrite object[] URL rewrite rules: [{from, to}] — first match wins
http2 bool false Enable HTTP/2 for upstream connections
timeout.connectMs number 3000 TCP connect timeout
timeout.sendMs number Request send timeout
timeout.readMs number 30000 Response read timeout
timeout.firstByteMs number Max ms to wait for first upstream response byte (overrides readMs)
timeout.perTryMs number Per-retry timeout
websocket bool false Allow WebSocket upgrades (101 Switching Protocols) — rejected with 502 by default
retry.attempts number 0 Number of retry attempts (0 = disabled)
retry.conditions string[] connection_error, 5xx, timeout
retry.backoffMs number 0 Wait between retries (ms)
retry.backoffJitter bool false ±50% random spread on backoffMs to avoid thundering herd
retry.budgetPercent number Soft cap: max % of in-flight requests that may be retries
healthCheck object Active health probes — see Health checks
backup string Fallback URL when all primaries are unhealthy
cache object Response cache — see Proxy cache
pool object Connection pool — see Connection pool
rateLimit object Per-route rate limit — see Rate limiting
upstreamTls object TLS for HTTPS upstreams — see Upstream TLS
mirror string Shadow URL — see Traffic mirroring
sticky.cookie string Cookie name for sticky sessions
sticky.secret string HMAC-SHA256 secret — Conduit signs + verifies the cookie (prevents forgery)
sticky.strict boolean false Return 503 when the pinned upstream is down (instead of routing elsewhere)
groups object[] Two-level LB groups: [{name, targets, strategy}]
groupStrategy string round-robin Outer strategy when groups is set
priority number (0–100) 50 Request priority for load shedding — see Priority routing

Routes

The routes array matches requests before proxy / static. First match wins.

# YAML
routes:
  # Route to dedicated API v2 server — checked first
  - match:
      path: /api/v2/**
      method: [GET, POST]
      headers:
        X-Version: "2"
    proxy:
      targets: ["http://v2-backend:4000"]

  # Write operations go to write cluster
  - match:
      path: /api/**
      method: [POST, PUT, PATCH, DELETE]
    proxy:
      targets: ["http://write-api:4001", "http://write-api:4002"]
      strategy: least-conn

  # Beta users via cookie → canary backend
  - match:
      cookies:
        beta: "1" # exact: cookie beta=1
        experiment: "blue|green" # regex: blue or green
    proxy:
      targets: ["http://canary:4000"]

  # Everything else → SPA static files
  - match:
      path: /**
    static: ./dist
// JSON
{
  "routes": [
    {
      "match": {
        "path": "/api/v2/**",
        "method": ["GET", "POST"],
        "headers": { "X-Version": "2" }
      },
      "proxy": { "targets": ["http://v2-backend:4000"] }
    },
    {
      "match": {
        "path": "/api/**",
        "method": ["POST", "PUT", "PATCH", "DELETE"]
      },
      "proxy": {
        "targets": ["http://write-api:4001", "http://write-api:4002"],
        "strategy": "least-conn"
      }
    },
    {
      "match": { "path": "/**" },
      "static": "./dist"
    }
  ]
}

Each route entry has exactly three top-level fields: match, proxy, and static. Auth, rate limiting, and other policies come from the site-level config and apply to all routes uniformly.

match field Type Description
path glob Path glob — see skipPaths glob syntax
method string[] HTTP methods to match (case-insensitive)
headers object Request headers that must match (exact or regex)
query object Query parameters that must match (exact or regex)
cookies object Cookies that must match (exact or regex)

All headers, query, and cookies values are matched as full-string regex (anchored ^…$). Plain strings like "v2" or "1" match exactly; regex patterns like "blue|green" or "Bearer .+" use regex semantics. An invalid regex falls back to exact-string comparison.


Load Balancing

Set strategy inside a proxy route. All strategies skip upstreams that are currently unhealthy (failed health probes) or ejected (outlier detection).

proxy:
  /api:
    targets: ["http://a:4000", "http://b:4000"]
    strategy: least-conn # pick a strategy

round-robin (default)

Cycles through the target list in order, one request at a time. A per-route atomic counter is incremented on each request and taken modulo the number of healthy upstreams.

Request 1 → a  Request 2 → b  Request 3 → a  …

Use when: all upstreams are homogeneous (same hardware, same capacity). Simple, predictable, zero overhead.

proxy:
  /api:
    targets: ["http://a:4000", "http://b:4000", "http://c:4000"]
    strategy: round-robin # or omit — this is the default

weighted-round-robin

Like round-robin, but each upstream gets a number of slots proportional to its weight. If A has weight 3 and B has weight 1, the pattern is A, A, A, B per four requests (exact interleaving may differ, but ratios are preserved over time).

Use when: upstreams have different capacities — e.g. a beefy primary and a smaller standby, or a canary deployment receiving a fraction of traffic.

Targets must be { url, weight } objects — plain strings use weight 1.

proxy:
  /api:
    targets:
      - { url: "http://primary:4000", weight: 9 } # 90% of traffic
      - { url: "http://canary:4000", weight: 1 } # 10% canary
    strategy: weighted-round-robin
// JSON
{
  "proxy": {
    "/api": {
      "targets": [
        { "url": "http://primary:4000", "weight": 9 },
        { "url": "http://canary:4000", "weight": 1 }
      ],
      "strategy": "weighted-round-robin"
    }
  }
}

Note: conduit upstreams weight can change weights at runtime without reloading the config.


least-conn

Routes each new request to the upstream with the fewest active in-flight connections at that instant. The connection counter is incremented before the request is forwarded and decremented when the response completes (including retries and errors).

Use when: requests have highly variable response times — e.g. a mix of fast reads and slow writes. Under uniform load it behaves like round-robin; its advantage emerges when some requests stall.

proxy:
  /api:
    targets: ["http://a:4000", "http://b:4000", "http://c:4000"]
    strategy: least-conn

Tip: combine with healthCheck.maxConnectionsPerUpstream for a circuit breaker that activates when all upstreams are saturated.


least-response-time

Routes each request to the upstream with the lowest measured latency from the most recent active health probe. Falls back to round-robin during the warm-up phase before any probe has completed.

Latency is measured by the health-check prober (HEAD request to healthCheck.path). Upstreams without probe data are ranked last (u64::MAX).

Use when: upstreams have meaningfully different hardware or geographic proximity and you want to bias traffic toward the fastest one. Requires healthCheck to be configured on the route — without probes, this strategy falls back to round-robin permanently.

proxy:
  /api:
    targets:
      - "http://us-east:4000"
      - "http://eu-west:4000"
    strategy: least-response-time
    healthCheck:
      path: /health
      intervalSecs: 10
// JSON
{
  "proxy": {
    "/api": {
      "targets": ["http://us-east:4000", "http://eu-west:4000"],
      "strategy": "least-response-time",
      "healthCheck": { "path": "/health", "intervalSecs": 10 }
    }
  }
}

ip-hash

Hashes the client IP address (FNV-1a) and maps it to an upstream via hash % pool_size. The same IP always hits the same upstream — as long as the pool size doesn't change.

Use when: you need soft session affinity without a session cookie, e.g. legacy apps that store state per-IP, or to concentrate logs from one user on one backend.

Caveat: adding or removing an upstream changes pool_size and remaps roughly half of all clients. This is hash % N (modulo), not a consistent hash ring. Use sticky.cookie for stable, cookie-based affinity.

proxy:
  /auth:
    targets: ["http://auth1:5000", "http://auth2:5000"]
    strategy: ip-hash
    hashKey: ip # default for ip-hash; can be omitted
// JSON
{
  "proxy": {
    "/auth": {
      "targets": ["http://auth1:5000", "http://auth2:5000"],
      "strategy": "ip-hash"
    }
  }
}

consistent-hash

Hashes a configurable hashKey (IP, URL, or any request header) and maps it to an upstream via hash % pool_size. Identical to ip-hash in implementation — the distinction is purely which value is hashed.

Use when: you want to route requests by tenant, user, or any other request attribute to a dedicated upstream.

The hashKey field controls what is hashed:

hashKey value Hashes Example use case
ip Client IP Per-IP affinity
url Full request URL Cache-locality — same URL always hits same backend
header:X-Name Value of header X-Name Per-tenant or per-user routing
proxy:
  /api:
    targets:
      ["http://shard-1:4000", "http://shard-2:4000", "http://shard-3:4000"]
    strategy: consistent-hash
    hashKey: "header:X-Tenant-ID"
// JSON
{
  "proxy": {
    "/api": {
      "targets": [
        "http://shard-1:4000",
        "http://shard-2:4000",
        "http://shard-3:4000"
      ],
      "strategy": "consistent-hash",
      "hashKey": "header:X-Tenant-ID"
    }
  }
}

Same caveat as ip-hash: pool size changes remap a large fraction of keys. This is hash % N, not a Karger consistent hash ring.


random

Selects an upstream uniformly at random on each request using a fast thread-local RNG.

Use when: you want an even distribution without any coordination overhead — useful for very large pools where maintaining a round-robin counter per route is unnecessary. In practice, round-robin is usually preferred since it provides better uniformity over small sample sizes.

proxy:
  /api:
    targets: ["http://a:4000", "http://b:4000", "http://c:4000"]
    strategy: random

p2c (Power of Two Choices)

On each request, samples two upstreams at random and forwards to the one with fewer active connections. Ties are broken in favour of the first sample.

Achieves O(log log N) maximum load imbalance — dramatically better tail latency than pure random, and competitive with least-conn at scale, with O(1) selection cost (no scan of the full pool).

With a single upstream in the pool, P2C falls back to round-robin.

Use when: the upstream pool is large (10+) and least-conn would scan the entire list on every request. Also useful as a drop-in replacement for least-conn when you want reduced coordination overhead.

proxy:
  /api:
    targets:
      - "http://a:4000"
      - "http://b:4000"
      - "http://c:4000"
      - "http://d:4000"
    strategy: p2c
// JSON
{
  "proxy": {
    "/api": {
      "targets": [
        "http://a:4000",
        "http://b:4000",
        "http://c:4000",
        "http://d:4000"
      ],
      "strategy": "p2c"
    }
  }
}

Strategy comparison

Strategy Session affinity Handles variable load Pool change impact Best for
round-robin none Homogeneous, stateless services
weighted-round-robin none Mixed-capacity pools, canary rollouts
least-conn none Variable request duration (mixed workloads)
least-response-time none Multi-region, geographically dispersed upstreams
ip-hash by IP remaps ~50% Soft affinity without cookies
consistent-hash by key remaps ~50% Per-tenant / per-key routing
random none Very large pools, simple distribution
p2c none Large pools, low-overhead load awareness

Sticky sessions

Route a client to the same upstream for the duration of a session cookie, using consistent hashing on the cookie value.

proxy:
  /app:
    targets: ["http://a:4000", "http://b:4000"]
    strategy: consistent-hash
    sticky:
      cookie: session_id
// JSON
{
  "proxy": {
    "/app": {
      "targets": ["http://a:4000", "http://b:4000"],
      "strategy": "consistent-hash",
      "sticky": { "cookie": "session_id" }
    }
  }
}

If the client presents no cookie (first request), the request is routed by the configured strategy and the upstream is recorded — no cookie is set by Conduit. The application is responsible for setting the session cookie.

HMAC-signed sticky cookies

Without a secret, the cookie value is used as a raw consistent-hash key. An attacker can craft a cookie to force routing to any backend they choose.

Set sticky.secret to make Conduit sign cookies with HMAC-SHA256 and verify them on every request. Forged cookies silently fall back to normal load-balancing rather than pinning to an attacker-controlled backend.

proxy:
  /app:
    targets: ["http://a:4000", "http://b:4000"]
    strategy: consistent-hash
    sticky:
      cookie: srv_id       # Conduit sets this cookie on every response
      secret: "$STICKY_SECRET"  # HMAC key — use an env var, never hardcode
      strict: false        # true = 503 when pinned upstream is down
Option Behaviour
No secret Cookie value is the raw consistent-hash key (legacy, no forgery protection)
secret set Conduit signs the URL with HMAC-SHA256 and injects a Set-Cookie on every response
strict: false (default) If the pinned upstream is unhealthy, fall back to the next available backend
strict: true If the pinned upstream is unhealthy, return 503 Service Unavailable immediately

The injected cookie attributes are: Path=/; HttpOnly; SameSite=Lax.

Security note: store the HMAC secret in an environment variable (secret: "$STICKY_SECRET"). Rotate by changing the value and reloading; existing cookies will silently fall through to normal load-balancing for one request and then get a new signed cookie.


Upstream groups (two-level balancing)

An outer strategy picks the group; an inner strategy balances within it. groups is an array — each entry has name, targets, and optional strategy.

proxy:
  /api:
    groups:
      - name: us-east
        targets: ["http://us-east-1:4000", "http://us-east-2:4000"]
        strategy: least-conn
      - name: eu-west
        targets: ["http://eu-west-1:4000", "http://eu-west-2:4000"]
        strategy: least-conn
    groupStrategy: ip-hash # outer: same client IP always hits same region
// JSON
{
  "proxy": {
    "/api": {
      "groups": [
        {
          "name": "us-east",
          "targets": ["http://us-east-1:4000", "http://us-east-2:4000"],
          "strategy": "least-conn"
        },
        {
          "name": "eu-west",
          "targets": ["http://eu-west-1:4000", "http://eu-west-2:4000"],
          "strategy": "least-conn"
        }
      ],
      "groupStrategy": "ip-hash"
    }
  }
}

See examples/upstream-groups.yaml


Static Files

Simple directory

# YAML
static: ./dist
// JSON
{ "static": "./dist" }

Multiple directories

# YAML
static:
  - ./dist
  - ./public
// JSON
{ "static": ["./dist", "./public"] }

Path-mapped directories

# YAML
static:
  /: ./dist
  /docs: ./docs-dist
// JSON
{ "static": { "/": "./dist", "/docs": "./docs-dist" } }

Static options

# YAML
static: ./dist
staticOptions:
  index: [index.html] # default files for directory requests
  dotFiles: ignore # "ignore" | "allow" | "deny"
  preCompressed: true # serve .br / .gz if present
  etag: true # ETag / 304 Not Modified support
  lastModified: true # Last-Modified header
  maxAge: "1y" # Cache-Control max-age (humantime: "1d", "30m", "1y")
// JSON
{
  "static": "./dist",
  "staticOptions": {
    "index": ["index.html"],
    "dotFiles": "ignore",
    "preCompressed": true,
    "etag": true,
    "lastModified": true,
    "maxAge": "1y"
  }
}
Field Type Default Description
index string[] ["index.html"] Default files for directory requests
dotFiles string "ignore" "ignore", "allow", or "deny"
preCompressed bool false Serve pre-compressed .br / .gz files
etag bool true ETag + conditional GET support
lastModified bool true Last-Modified header
maxAge string Cache-Control: max-age — humantime duration string

Redirects

redirects is an array of redirect rules.

# YAML
redirects:
  - from: /old-path
    to: "https://example.com/new-path"
    status: 301

  - from: "/blog/(.+)" # regex capture group
    to: "https://blog.example.com/$1"
    status: 308
// JSON
{
  "redirects": [
    {
      "from": "/old-path",
      "to": "https://example.com/new-path",
      "status": 301
    },
    { "from": "/blog/(.+)", "to": "https://blog.example.com/$1", "status": 308 }
  ]
}
Field Type Default Description
from string Path or regex pattern to match
to string Destination URL (capture groups $1$N are expanded)
status number 301 HTTP redirect status code

Fallback

Return a response when no route matches.

# YAML — SPA: serve index.html for all unmatched browser routes
fallback:
  file: ./dist/index.html
  status: 200
# YAML — content-type-aware fallback (Accept header negotiation)
fallback:
  byAccept:
    html:
      file: ./dist/index.html
      status: 200
    json:
      body: { "error": "Not Found", "status": 404 }
      status: 404
  status: 404
  body: "Not Found"
// JSON
{
  "fallback": {
    "file": "./dist/index.html",
    "status": 200
  }
}
// JSON
{
  "fallback": {
    "byAccept": {
      "html": { "file": "./dist/index.html", "status": 200 },
      "json": { "body": { "error": "Not Found", "status": 404 }, "status": 404 }
    },
    "status": 404,
    "body": "Not Found"
  }
}
Field Type Default Description
file path File to serve
body any Response body (string or JSON object)
status number 200 HTTP status code
headers object Response headers to set
byAccept object Content-type-aware rules keyed by Accept type (html, json, *)

Health Checks

Active upstream probes

# YAML
proxy:
  /api:
    targets: ["http://a:4000", "http://b:4000"]
    healthCheck:
      path: /health
      intervalSecs: 10
      unhealthyThreshold: 3
      healthyThreshold: 1
      slowStartSecs: 30
      unhealthyStatus: [429, 500, 502, 503, 504] # treat these status codes as failures
      unhealthyLatencyMs: 2000 # treat responses slower than 2s as failures
// JSON
{
  "proxy": {
    "/api": {
      "targets": ["http://a:4000", "http://b:4000"],
      "healthCheck": {
        "path": "/health",
        "intervalSecs": 10,
        "unhealthyThreshold": 3,
        "healthyThreshold": 1,
        "slowStartSecs": 30,
        "unhealthyStatus": [429, 500, 502, 503, 504],
        "unhealthyLatencyMs": 2000
      }
    }
  }
}

Site health endpoint (for load balancer probes)

# YAML
healthCheck: true

healthCheck:
  path: /__health__
  includeUpstreams: true   # include per-upstream health in JSON response
// JSON
{ "healthCheck": { "includeUpstreams": true } }

Health check field reference

Field Type Default Description
path string /__health__ Probe URL path
intervalSecs number 10 Probe interval
timeoutMs number 2000 Probe timeout
unhealthyThreshold number 3 Consecutive failures before removal
healthyThreshold number 1 Consecutive passes before re-adding
unhealthyStatus number[] any non-2xx HTTP status codes from the health-check probe that count as failures. Default: any non-2xx response. Example: [429, 500, 502, 503, 504]
unhealthyLatencyMs number Health-check probe responses slower than this (ms) count as failures, even if the status code is 2xx
slowStartSecs number 0 Traffic ramp-up period after recovery
maxConnectionsPerUpstream number Circuit breaker threshold
prewarmConnections number 0 Pre-establish N keepalive connections at startup (max 8)
includeUpstreams bool false Include upstream health in /__health__ response

Circuit Breaker

When all upstreams reach maxConnectionsPerUpstream concurrent connections, Conduit returns 503 immediately instead of queuing.

# YAML
proxy:
  /api:
    targets: ["http://a:4000", "http://b:4000", "http://c:4000"]
    healthCheck:
      maxConnectionsPerUpstream: 100
    backup: "http://replica:4000"
// JSON
{
  "proxy": {
    "/api": {
      "targets": ["http://a:4000", "http://b:4000", "http://c:4000"],
      "healthCheck": { "maxConnectionsPerUpstream": 100 },
      "backup": "http://replica:4000"
    }
  }
}

See examples/circuit-breaker.yaml


Retry

# YAML
proxy:
  /api:
    targets: ["http://a:4000", "http://b:4000"]
    retry:
      attempts: 3
      conditions:
        - connection_error
        - "5xx"
        - timeout
      backoffMs: 100
      backoffJitter: true # add ±50% random spread to backoffMs
      budgetPercent: 20
    timeout:
      perTryMs: 2000
// JSON
{
  "proxy": {
    "/api": {
      "targets": ["http://a:4000", "http://b:4000"],
      "retry": {
        "attempts": 3,
        "conditions": ["connection_error", "5xx", "timeout"],
        "backoffMs": 100,
        "backoffJitter": true,
        "budgetPercent": 20
      },
      "timeout": { "perTryMs": 2000 }
    }
  }
}

Retry field reference

Field Type Default Description
attempts number 0 Number of retry attempts (0 = disabled)
conditions string[] Triggers: "connection_error", "5xx", "timeout". Retries only happen when at least one matches.
backoffMs number 0 Fixed wait between attempts (ms). 0 = immediate retry.
backoffJitter bool false Apply ±50% random spread to backoffMs to avoid thundering herd. Effective delay: [ms/2, ms*3/2)
budgetPercent number Soft cap: at most this fraction of in-flight requests may be retries. Prevents retry storms under mass failure.

Body buffering for retry — POST/PUT/PATCH requests are only retried when limits.maxBodyBufferBytes is set. Without it, request bodies are not buffered and connection_error retries on non-GET methods are skipped silently.

1xx interim responses

Some upstream backends (Spring Boot, CDNs, gRPC gateways) send one or more 1xx informational responses before the final response — for example 103 Early Hints for browser resource preloading, or 100 Continue after a Expect: 100-continue request header.

Conduit passes 1xx responses through to the client without running any middleware (retry logic, error masking, response transforms, etc.), then continues waiting for the real response. No configuration is required.

Exception: 101 Switching Protocols (WebSocket upgrade) is handled separately by the websocket: true route option — see the field reference table.


Outlier Detection

Passively eject upstreams that return too many 5xx responses from real traffic.

# YAML
outlierDetection:
  consecutive5xx: 5
  baseEjectionTimeSecs: 30
  maxEjectionTimeSecs: 300
  maxEjectionPercent: 33
// JSON
{
  "outlierDetection": {
    "consecutive5xx": 5,
    "baseEjectionTimeSecs": 30,
    "maxEjectionTimeSecs": 300,
    "maxEjectionPercent": 33
  }
}

Ejection uses exponential backoff: 30 s → 60 s → 120 s → … up to maxEjectionTimeSecs.

Half-open circuit breaker: when the ejection period expires, the first request is allowed through as a probe. If the probe succeeds (non-5xx), the upstream is fully restored and ejection_count is reset. If it fails, the upstream is re-ejected with the next backoff level. All other requests during the probe are blocked until the probe completes.

Field Type Default Description
consecutive5xx number 5 Consecutive errors before ejection
baseEjectionTimeSecs number 30 Initial ejection duration
maxEjectionTimeSecs number 300 Maximum ejection duration (cap on backoff)
maxEjectionPercent number 10 Max % of cluster that may be ejected at once

Limits

# YAML
limits:
  maxBodyBytes: 10485760 # reject request bodies over 10 MB (413)
  maxHeaderBytes: 65536 # reject headers over 64 KB
  timeoutSecs: 30 # global request timeout (seconds)
  maxInflightRequests: 1000 # return 503 when 1000 requests are in flight
  maxBodyBufferBytes: 1048576 # buffer up to 1 MB per request for retry replay
  maxConnectionsPerIp: 50 # max simultaneous connections from one IP (429)
  keepaliveRequestLimit: 1000 # recycle connections after this many requests
  priorityThreshold: 0.8 # shed low-priority routes above 80% concurrency
  minUploadRateBytesPerSec: 1024 # reject uploads slower than 1 KiB/s (408)
// JSON
{
  "limits": {
    "maxBodyBytes": 10485760,
    "maxHeaderBytes": 65536,
    "timeoutSecs": 30,
    "maxInflightRequests": 1000,
    "maxBodyBufferBytes": 1048576,
    "maxConnectionsPerIp": 50,
    "keepaliveRequestLimit": 1000,
    "priorityThreshold": 0.8,
    "minUploadRateBytesPerSec": 1024
  }
}
Field Type Default Description
maxBodyBytes number Max request body size — returns 413 if exceeded
maxHeaderBytes number Max request header size
timeoutSecs number Global request timeout
maxInflightRequests number Max concurrent requests — returns 503 if exceeded (must be ≥ 1)
maxBodyBufferBytes number Max body buffered per request for retry replay
maxConnectionsPerIp number Max simultaneous open connections from a single client IP — returns 429 if exceeded
maxRequestHeaders number Max number of request headers — returns 431 Request Header Fields Too Large if exceeded
keepaliveRequestLimit number Max requests per keepalive connection; closes and recycles after. Equivalent to nginx's keepalive_requests.
priorityThreshold number 0.8 Fraction of maxInflightRequests at which low-priority routes are shed (0.0–1.0) — see Priority routing
minUploadRateBytesPerSec number Minimum upload rate in bytes/s — closes slow uploads with 408 Request Timeout (slow-loris protection)

Priority Routing

Priority routing lets high-value routes continue to be served when the site is under load, while low-priority routes are shed with 503 Load Shedding.

How it works

  1. Set limits.maxInflightRequests to cap total concurrency.
  2. Set limits.priorityThreshold (default 0.8) — the fraction of the cap at which shedding begins.
  3. Mark routes with priority: 0–100 (50 = normal, omitted = normal).
  4. When inflight / maxInflightRequests ≥ priorityThreshold, any request whose effective priority is below 50 receives 503 Load Shedding.

Requests with priority ≥ 50 (normal or high) are never shed by this mechanism. The X-Priority: <0–100> request header can raise the effective priority above the configured route value (useful for trusted internal callers).

# YAML
limits:
  maxInflightRequests: 2000
  priorityThreshold: 0.8 # shed low-priority at 1600+ concurrent

routes:
  - match:
      path: /api/critical/**
    proxy:
      targets: [http://api:4000]
      priority: 90 # always served

  - match:
      path: /api/batch/**
    proxy:
      targets: [http://api:4000]
      priority: 10 # shed first when overloaded

  - match:
      path: /api/**
    proxy:
      targets: [http://api:4000]
      # no priority → defaults to 50 (normal, not shed)
{
  "limits": {
    "maxInflightRequests": 2000,
    "priorityThreshold": 0.8
  },
  "routes": [
    {
      "match": { "path": "/api/critical/**" },
      "proxy": { "targets": ["http://api:4000"], "priority": 90 }
    },
    {
      "match": { "path": "/api/batch/**" },
      "proxy": { "targets": ["http://api:4000"], "priority": 10 }
    }
  ]
}
Field Type Default Description
limits.priorityThreshold number 0.8 Load fraction at which shedding begins (0.0–1.0)
proxy.*.priority number (0–100) 50 Route priority; below 50 = sheddable

Note: Priority routing only applies when both maxInflightRequests and priorityThreshold are configured on the site.


Fault Injection

Requires cargo build --features fault-injection

For testing only — do not use in production.

# YAML
faultInjection:
  abort:
    percent: 5
    status: 503
    body: "Injected fault"
  delay:
    percent: 10
    ms: 500
// JSON
{
  "faultInjection": {
    "abort": { "percent": 5, "status": 503, "body": "Injected fault" },
    "delay": { "percent": 10, "ms": 500 }
  }
}

Proxy Cache

Requires cargo build --features cache For Redis-backed cache also add --features redis; for disk cache add --features disk-cache.

# YAML
proxy:
  /api:
    targets: ["http://backend:4000"]
    cache:
      store: memory
      ttlSecs: 60
      maxSizeMb: 256 # evict LRU entries after 256 MB
      staleWhileRevalidateSecs: 300 # serve stale up to 5 min while refreshing
      staleIfErrorSecs: 600 # serve stale up to 10 min if upstream fails
      varyHeaders: [Accept-Language, Accept-Encoding]
      skipPaths: [/api/me, /api/cart] # never cache these paths
      skipIfCookie: true
      methods: [GET, HEAD]
// JSON
{
  "proxy": {
    "/api": {
      "targets": ["http://backend:4000"],
      "cache": {
        "store": "memory",
        "ttlSecs": 60,
        "maxSizeMb": 256,
        "staleWhileRevalidateSecs": 300,
        "staleIfErrorSecs": 600,
        "varyHeaders": ["Accept-Language", "Accept-Encoding"],
        "skipPaths": ["/api/me", "/api/cart"],
        "skipIfCookie": true,
        "methods": ["GET", "HEAD"]
      }
    }
  }
}

Cache field reference

Field Type Default Description
store string "memory", "redis://..." / "rediss://..." (--features redis), "disk:/path" (--features disk-cache)
ttlSecs number Fresh cache TTL (seconds)
maxSizeMb number Memory budget; LRU eviction above this
staleWhileRevalidateSecs number 0 Serve stale while refreshing in background (RFC 5861)
staleIfErrorSecs number 0 Serve stale when upstream returns 5xx, including after retries are exhausted (RFC 5861)
earlyRefreshSecs number 0 Refresh cache in the background when remaining TTL < this value (see below)
varyHeaders string[] Vary cache key by these request headers
skipPaths string[] Paths to never cache
skipIfCookie bool false Skip caching when request has a cookie
methods string[] [GET, HEAD] Cacheable HTTP methods

Cache keyscheme + host + path + query string. Request body is never part of the key, so POST responses are not cached by default (add "POST" to methods only for idempotent endpoints). Use varyHeaders to differentiate responses by Accept-Language or Accept-Encoding.

Age header (RFC 7234 §5.1): Conduit automatically injects an Age: <seconds> header on every cache hit, computed as now − Date from the stored response. Any Age header carried by the cached response is replaced to prevent double-counting across proxy hops. No configuration is required.

Stale-while-revalidate (RFC 5861): the first request after TTL expiry returns the stale response immediately while a background request refreshes the cache. Zero latency penalty for users. A built-in cache lock prevents thundering herd — only one background fetch goes to the upstream at a time.

Stale-if-error (RFC 5861): when the upstream returns a 5xx error, Conduit serves the last known good cached response instead of forwarding the error to the client. This works in all three scenarios:

  1. No retry configured — upstream 5xx immediately falls back to stale cache.
  2. Retry configured, budget exhausted — after all retry attempts fail, stale cache is served instead of the final 5xx error.
  3. Retry + stale together — Conduit retries the configured number of times; if all retries fail and stale cache is available, it serves stale.

Set staleIfErrorSecs to the maximum age (in seconds) of a stale response you are willing to serve. When no stale entry exists or the stale entry is older than staleIfErrorSecs, the upstream error is forwarded to the client.

Early refresh (earlyRefreshSecs): when the remaining TTL of a cached entry drops below earlyRefreshSecs, Conduit fires a background GET request directly to the upstream while the current client request is still served the (still-valid) cached response with zero latency. The cache is updated before it ever expires, so clients never see stale content as long as the upstream is reachable.

Comparison with staleWhileRevalidateSecs:

Feature Activates Clients see stale?
staleWhileRevalidateSecs After TTL expires Yes, until refresh
earlyRefreshSecs Before TTL expires No

Use earlyRefreshSecs when zero-stale is important (news feeds, pricing data, session-sensitive API). Use staleWhileRevalidateSecs when occasional stale is acceptable and you want a simpler setup.

# Never-stale cache: refresh 10 s before the 60-second TTL expires.
cache:
  store: memory
  ttlSecs: 60
  earlyRefreshSecs: 10

Source: h2o lib/common/cache.cH2O_CACHE_FLAG_EARLY_UPDATE.

s-maxage handling: Conduit respects the upstream Cache-Control: s-maxage=N directive as the effective TTL when the upstream returns it. s-maxage=0 explicitly prevents caching regardless of ttlSecs. When the upstream returns no Cache-Control directive, ttlSecs from the route config is used. ttlSecs in the config always caps the maximum TTL — upstream headers cannot extend it beyond the configured limit.

Upstream Cache-Control Effect
not present Use ttlSecs from config
s-maxage=N Use min(N, ttlSecs)
s-maxage=0 Do not cache this response
no-store or private Do not cache this response

Cache store options

"memory" — in-process cache, lost on restart. Best for single-instance deployments with small response bodies.

"redis://host:port" / "rediss://host:port" — shared Redis cache. (--features redis required) All Conduit instances share the same cache — consistent hit rate under horizontal scaling. rediss:// enables TLS (AWS ElastiCache, Azure Cache).

# conduit.yaml — Redis cache shared across multiple instances
proxy:
  /api:
    targets: ["http://api1:4000", "http://api2:4000"]
    cache:
      store: "redis://redis:6379"
      ttlSecs: 300
      staleWhileRevalidateSecs: 60
      varyHeaders: [Accept-Language]
// conduit.json
{
  "proxy": {
    "/api": {
      "targets": ["http://api1:4000", "http://api2:4000"],
      "cache": {
        "store": "redis://redis:6379",
        "ttlSecs": 300,
        "staleWhileRevalidateSecs": 60
      }
    }
  }
}

If Redis is unreachable at startup or during a request, caching is silently disabled for that request — the proxy continues to work normally (fail-open).

"disk:/path/to/dir" — filesystem cache, survives restarts. (--features disk-cache required) Useful for large response bodies or when Redis is not available.

proxy:
  /assets:
    targets: ["http://assets:4000"]
    cache:
      store: "disk:/var/cache/conduit"
      ttlSecs: 86400 # 1 day
{
  "proxy": {
    "/assets": {
      "targets": ["http://assets:4000"],
      "cache": { "store": "disk:/var/cache/conduit", "ttlSecs": 86400 }
    }
  }
}

See examples/stale-while-revalidate.yaml


Basic Auth

# YAML
basicAuth:
  users:
    alice: "$ALICE_PASSWORD"
    bob: "$BOB_PASSWORD"
  realm: "My App"
  challenge: true # send WWW-Authenticate header (default: true)
  skipPaths: [/__health__]
// JSON
{
  "basicAuth": {
    "users": { "alice": "$ALICE_PASSWORD", "bob": "$BOB_PASSWORD" },
    "realm": "My App",
    "challenge": true,
    "skipPaths": ["/__health__"]
  }
}
Field Type Default Description
users object { username: password } map
realm string "Conduit" Shown in browser login dialog
challenge bool true Whether to send WWW-Authenticate header
skipPaths string[] Paths that bypass Basic Auth — see glob syntax

API Key

# YAML
apiKey:
  keys:
    - "$PRIMARY_API_KEY"
    - "$SECONDARY_API_KEY"
  header: X-API-Key
  skipPaths: [/__health__, /public/**]
// JSON
{
  "apiKey": {
    "keys": ["$PRIMARY_API_KEY", "$SECONDARY_API_KEY"],
    "header": "X-API-Key",
    "skipPaths": ["/__health__", "/public/**"]
  }
}

The key may be sent in the configured header or as a ?api_key= query parameter.


JWT Auth

Requires cargo build --features jwt

Validates Authorization: Bearer <token> on every request.

JWKS endpoint (recommended for production)

# YAML
jwtAuth:
  jwksUrl: "https://YOUR_DOMAIN.auth0.com/.well-known/jwks.json"
  audience: ["https://api.example.com"]
  issuer: "https://YOUR_DOMAIN.auth0.com"
  jwksRefreshSecs: 3600
  skipPaths: [/__health__, /public/**]
// JSON
{
  "jwtAuth": {
    "jwksUrl": "https://YOUR_DOMAIN.auth0.com/.well-known/jwks.json",
    "audience": ["https://api.example.com"],
    "issuer": "https://YOUR_DOMAIN.auth0.com",
    "jwksRefreshSecs": 3600,
    "skipPaths": ["/__health__", "/public/**"]
  }
}

Shared secret (HS256)

jwtAuth:
  secret: "$JWT_SECRET"
  skipPaths: [/__health__]
// JSON
{ "jwtAuth": { "secret": "$JWT_SECRET", "skipPaths": ["/__health__"] } }

JWT field reference

Field Type Default Description
secret string HS256 shared secret (mutually exclusive with jwksUrl)
jwksUrl string JWKS endpoint URL (RS256 / ES256)
jwksRefreshSecs number 3600 JWKS key refresh interval
audience string[] Required aud claims
issuer string Required iss claim
skipPaths string[] Paths that bypass JWT validation

Injecting JWT claims as upstream headers

requestTransform:
  setHeaders:
    X-User-ID: "{{ jwt.sub }}"
    X-User-Email: "{{ jwt.email }}"
    X-Tenant: "{{ jwt.tid }}"
  removeHeaders:
    - Authorization
// JSON
{
  "requestTransform": {
    "setHeaders": {
      "X-User-ID": "{{ jwt.sub }}",
      "X-User-Email": "{{ jwt.email }}"
    },
    "removeHeaders": ["Authorization"]
  }
}

Unknown claims expand to empty string. See Request / Response Transform.

See examples/jwt-auth.yaml


Forward Auth

Requires cargo build --features forward-auth

Delegate authentication to an external HTTP service.

Client -> Conduit -> Auth service
2xx -> copy responseHeaders, forward to upstream
4xx -> return to client, stop
fail -> 401 (fail closed)
# YAML
forwardAuth:
  url: "http://auth-service:9000/verify"
  requestHeaders: [Authorization, Cookie]
  responseHeaders: [X-User-ID, X-Role]
  timeoutMs: 3000
  skipPaths: [/__health__, /public/**]
// JSON
{
  "forwardAuth": {
    "url": "http://auth-service:9000/verify",
    "requestHeaders": ["Authorization", "Cookie"],
    "responseHeaders": ["X-User-ID", "X-Role"],
    "timeoutMs": 3000,
    "skipPaths": ["/__health__", "/public/**"]
  }
}
Field Type Default Description
url string Auth service URL (required)
requestHeaders string[] Client headers to forward to auth service
responseHeaders string[] Auth response headers to inject into upstream request
timeoutMs number 5000 Auth service timeout
skipPaths string[] Paths that bypass forward auth

The auth service receives X-Forwarded-Method, X-Forwarded-Uri, X-Forwarded-For, plus any requestHeaders.

See examples/forward-auth.yaml


Consumers

Requires cargo build --features consumers JWT consumers (V2 / V3) additionally require --features jwt.

Named API clients with per-consumer credentials, rate limits, and headers. After identification, the consumer's username is injected as X-Consumer-ID. Unidentified requests receive 401.

# YAML — V1 (API key / Basic Auth) + V2 (per-consumer JWT)
consumers:
  idHeader: "X-Consumer-ID"
  apiKeyHeader: "X-API-Key"
  skipPaths: [/__health__]

  consumers:
    # V1: API key
    - username: alice
      apiKey: "$ALICE_KEY"
      rateLimit: { windowSecs: 60, limit: 100 }
      headers: { X-Tier: free }

    # V1: Basic Auth (username from consumer.username, password from basicAuth.password)
    - username: billing-service
      basicAuth: { password: "$BILLING_PASSWORD" }
      headers: { X-Internal: "true" }

    # V2: JWT with HS256 secret — token must be signed with this secret
    - username: mobile-app
      jwt:
        secret: "$MOBILE_JWT_SECRET"
        issuer: "https://auth.internal"
      rateLimit: { windowSecs: 60, limit: 500 }

    # V2: JWT with JWKS — token validated against public keys from this endpoint
    - username: partner-app
      jwt:
        jwksUrl: "https://partner.example.com/.well-known/jwks.json"
        audience: ["my-api"]
// JSON
{
  "consumers": {
    "idHeader": "X-Consumer-ID",
    "skipPaths": ["/__health__"],
    "consumers": [
      {
        "username": "alice",
        "apiKey": "$ALICE_KEY",
        "rateLimit": { "windowSecs": 60, "limit": 100 },
        "headers": { "X-Tier": "free" }
      },
      {
        "username": "billing-service",
        "basicAuth": { "password": "$BILLING_PASSWORD" },
        "headers": { "X-Internal": "true" }
      },
      {
        "username": "mobile-app",
        "jwt": { "secret": "$MOBILE_JWT_SECRET" },
        "rateLimit": { "windowSecs": 60, "limit": 500 }
      },
      {
        "username": "partner-app",
        "jwt": {
          "jwksUrl": "https://partner.example.com/.well-known/jwks.json",
          "audience": ["my-api"]
        }
      }
    ]
  }
}

V3: Shared JWT (Auth0 / Cognito / Keycloak pattern)

One JWKS endpoint for all consumers; consumers are identified by sub claim.

# YAML
consumers:
  sharedJwt:
    jwksUrl: "https://YOUR_DOMAIN.auth0.com/.well-known/jwks.json"
    audience: ["https://api.example.com"]
    issuer: "https://YOUR_DOMAIN.auth0.com"
    usernameClaim: "sub" # default

  consumers:
    - username: "auth0|alice123"
      rateLimit: { windowSecs: 60, limit: 100 }
      headers: { X-Tier: free }

    - username: "auth0|bob456"
      rateLimit: { windowSecs: 60, limit: 10000 }
      headers: { X-Tier: premium }
// JSON
{
  "consumers": {
    "sharedJwt": {
      "jwksUrl": "https://YOUR_DOMAIN.auth0.com/.well-known/jwks.json",
      "audience": ["https://api.example.com"],
      "issuer": "https://YOUR_DOMAIN.auth0.com",
      "usernameClaim": "sub"
    },
    "consumers": [
      {
        "username": "auth0|alice123",
        "rateLimit": { "windowSecs": 60, "limit": 100 }
      },
      {
        "username": "auth0|bob456",
        "rateLimit": { "windowSecs": 60, "limit": 10000 }
      }
    ]
  }
}

ConsumersConfig field reference

Field Type Default Description
consumers object[] List of named consumers
idHeader string x-consumer-id Header injected into upstream with consumer username
apiKeyHeader string x-api-key Header to read API keys from
skipPaths string[] Paths that bypass consumer auth
sharedJwt object Single JWKS for all consumers, identified by sub claim — Auth0/Cognito/Keycloak pattern (--features jwt required)

Per-consumer fields

Field Type Description
username string Required — unique name, injected as X-Consumer-ID
apiKey string API key credential
basicAuth object { password } — username is taken from consumer.username
jwt object { secret? jwksUrl? audience? issuer? } — per-consumer JWT identification (--features jwt required)
rateLimit object Per-consumer rate limit (global, not per-IP)
headers object Additional headers to inject into upstream request

See examples/consumers.yaml


Rate Limiting

In-memory rate limiting requires no feature flag — available in every build, including the minimal default = []. store: "redis://..." requires --features redis.

Site-level

Applied to all requests before authentication.

# YAML
rateLimit:
  windowSecs: 60
  limit: 1000
  keyBy: ip
  store: memory # or "redis://host:port" for multi-instance
  skipPaths: [/__health__]
// JSON
{
  "rateLimit": {
    "windowSecs": 60,
    "limit": 1000,
    "keyBy": "ip",
    "store": "memory",
    "skipPaths": ["/__health__"]
  }
}

Per-route

Applied after routing, independently of the site-level limit.

proxy:
  /api/payments:
    targets: ["http://payments:4000"]
    rateLimit:
      windowSecs: 60
      limit: 10
      keyBy: "header:X-User-ID"
// JSON
{
  "proxy": {
    "/api/payments": {
      "targets": ["http://payments:4000"],
      "rateLimit": {
        "windowSecs": 60,
        "limit": 10,
        "keyBy": "header:X-User-ID"
      }
    }
  }
}

Rate limit field reference

Field Type Default Description
windowSecs number Sliding window duration (seconds) — required
limit number Max requests per key per window — required
burst number 0 Extra burst capacity above limit (see below)
keyBy string "ip" "ip" or "header:<name>"
store string "memory" "memory" or "redis://host:port" (--features redis required for Redis)
skipPaths string[] Paths that bypass rate limiting — see skipPaths glob syntax
dryRun bool false Log rate-limit violations without actually rejecting requests — useful for tuning limits before enforcement

The rate limiter uses a token-bucket algorithm. Tokens refill at limit / windowSecs per second. Without burst, the bucket holds limit tokens.

Burst capacity (burst: N): the bucket starts with limit + N tokens, allowing short spikes above the sustained rate. The refill rate stays at limit / windowSecs — burst is absorbed and not refilled.

# Allow up to 80 requests in a burst, sustained at 1 req/s
rateLimit:
  windowSecs: 60
  limit: 60
  burst: 20

Request / Response Transform

Static header transforms

# YAML
requestTransform:
  setHeaders:
    X-User-ID: "{{ jwt.sub }}"
    X-Gateway: conduit
  removeHeaders:
    - Authorization

responseTransform:
  setHeaders:
    X-Served-By: conduit
  removeHeaders:
    - Server
    - X-Powered-By
    - X-AspNet-Version
// JSON
{
  "requestTransform": {
    "setHeaders": { "X-User-ID": "{{ jwt.sub }}", "X-Gateway": "conduit" },
    "removeHeaders": ["Authorization"]
  },
  "responseTransform": {
    "setHeaders": { "X-Served-By": "conduit" },
    "removeHeaders": ["Server", "X-Powered-By", "X-AspNet-Version"]
  }
}

JWT claim templates

Requires cargo build --features jwt — templates expand to "" when JWT auth is not active.

Available in requestTransform.setHeaders after JWT validation:

Template Claim Notes
{{ jwt.sub }} sub User identifier — always present
{{ jwt.email }} email Email claim (if IdP includes it)
{{ jwt.iss }} iss Token issuer
any claim any {{ jwt.<claim> }} — unknown claims expand to ""

Traffic Mirroring

Send a copy of requests to a shadow backend. The shadow response is discarded — clients only see the primary response.

proxy:
  /api:
    targets: ["http://api-v1:4000"]
    mirror: "http://api-v2:4000"
// JSON
{
  "proxy": {
    "/api": {
      "targets": ["http://api-v1:4000"],
      "mirror": "http://api-v2:4000"
    }
  }
}

The mirrored request includes all original headers plus X-Mirrored-From: <host>. Mirror failures do not affect clients.

Note: request body is not mirrored — only headers are forwarded to the shadow backend. This is sufficient for observability and shadow testing of read workloads.


Custom Response Headers

Inject headers into every response site-wide. These are in addition to any headers set by responseTransform.

# YAML
headers:
  X-Environment: production
  X-API-Version: "3"
// JSON
{
  "headers": {
    "X-Environment": "production",
    "X-API-Version": "3"
  }
}

Logging

# YAML — shorthand
logging: dev         # colorized, short — for development
logging: json        # structured JSON — for production
logging: combined    # Apache Combined Log Format
// JSON
{ "logging": "json" }

Full config:

logging:
  format: json
  file: ./logs/access.log
  stripQuery: true # omit query string from logged path (e.g. /search?q=... → /search)
  skipPaths:
    - /__health__
    - /__metrics__
    - /favicon.ico
// JSON
{
  "logging": {
    "format": "json",
    "file": "./logs/access.log",
    "stripQuery": true,
    "skipPaths": ["/__health__", "/__metrics__", "/favicon.ico"]
  }
}
Field Type Default Description
format string dev Log format — see table below
file string Append access logs to this file path (in addition to stdout)
stripQuery bool false Remove query string from the logged path. Useful when queries contain PII or tokens
skipPaths string[] Glob patterns — requests matching these paths are not logged
Format Description
dev Colorized, short — development
combined Apache Combined Log Format
common Apache Common Log Format
short Short, no timestamps
json Structured JSON — Loki, Datadog, Splunk, ELK

JSON log fields

// JSON
{
  "time": "2026-01-15T14:23:01Z",
  "method": "GET",
  "path": "/api/users",
  "status": 200,
  "bytes": 1234,
  "duration_ms": 42,
  "upstream_ms": 38,
  "ip": "10.0.0.1",
  "request_id": "a1b2c3d4-e5f6-...",
  "upstream": "http://api-1:4000"
}
Field Description
duration_ms Total request time from accept to response sent (ms)
upstream_ms Time spent waiting for the upstream response (ms). Absent for local handlers (health, static)
request_id Value of X-Request-ID — auto-generated UUID v4 if absent
upstream Selected upstream URL

Metrics

# YAML
metrics:
  path: /__metrics__
  token: "$METRICS_TOKEN"
// JSON
{ "metrics": { "path": "/__metrics__", "token": "$METRICS_TOKEN" } }

Prometheus scrape config:

scrape_configs:
  - job_name: conduit
    static_configs: [{ targets: ["conduit-host:8080"] }]
    metrics_path: /__metrics__
    bearer_token: "my-token"

Per-upstream metrics

In addition to the site-level metrics, Conduit exposes per-upstream URL metrics:

Metric Type Labels Description
conduit_upstream_requests_total counter upstream, status Requests forwarded to each upstream (including retries)
conduit_upstream_latency_seconds histogram upstream Upstream response latency (request sent → response received)
conduit_upstream_active_connections gauge upstream In-flight requests currently being processed by each upstream

These complement conduit_upstream_errors_total{route} for diagnosing which specific backend is slow or returning errors.


OpenTelemetry Tracing

Requires cargo build --features otlp

# YAML
global:
  otlp:
    endpoint: "http://tempo:4317"
    serviceName: "my-api"
    sampleRate: 0.1
    timeoutMs: 5000
// JSON
{
  "global": {
    "otlp": {
      "endpoint": "http://tempo:4317",
      "serviceName": "my-api",
      "sampleRate": 0.1,
      "timeoutMs": 5000
    }
  }
}

Each span: method, path, status, duration_ms, upstream_url, request_id. 5xx responses set span status to ERROR.

Field Type Default Description
endpoint string gRPC OTLP endpoint URL
serviceName string service.name in all spans
sampleRate number 1.0 Fraction of requests to trace (0.0–1.0)
timeoutMs number 5000 Export timeout

See examples/observability.yaml


Hot Reload

Watch the config file for changes and reload without restarting.

# YAML
hotReload: true

hotReload:
  extensions: [html, css, js, ts, jsx, tsx]   # file types that trigger browser reload
// JSON
{ "hotReload": true }
// JSON
{ "hotReload": { "extensions": ["html", "css", "js"] } }

Hot-reloadable (no restart): proxy, static, routes, rateLimit, basicAuth, apiKey, jwtAuth, forwardAuth, consumers, middleware, logging, cors, securityHeaders, cache, outlierDetection, limits, requestTransform, responseTransform, maskErrors.

Requires cold restart: port, tls.cert/key, tls.versions/ciphers, workers, backlog, global.admin.bind.


Security Headers

securityHeaders: true — safe defaults

securityHeaders: true

Sets these four headers on every response:

HTTP header Value
X-Content-Type-Options nosniff
X-Frame-Options SAMEORIGIN
Referrer-Policy strict-origin-when-cross-origin
X-XSS-Protection 1; mode=block

Custom security headers

securityHeaders:
  hstsMaxAgeSecs: 63072000 # → Strict-Transport-Security: max-age=63072000; includeSubDomains
  csp: "default-src 'self'; script-src 'self'"
  xFrameOptions: DENY
  referrerPolicy: "strict-origin-when-cross-origin"
// JSON
{
  "securityHeaders": {
    "hstsMaxAgeSecs": 63072000,
    "csp": "default-src 'self'",
    "xFrameOptions": "DENY",
    "referrerPolicy": "strict-origin-when-cross-origin"
  }
}

Full example with all fields:

securityHeaders:
  hstsMaxAgeSecs: 63072000
  hstsIncludeSubDomains: true # add includeSubDomains to HSTS header
  hstsPreload: true # add preload to HSTS (see hstspreload.org)
  csp: "default-src 'self'"
  xFrameOptions: DENY
  referrerPolicy: "no-referrer"
  permissionsPolicy: "geolocation=(), microphone=()"
  allowedHosts: # reject Host headers not in this list (→ 421)
    - "example.com"
    - "www.example.com"
Field Type Default (object form) Sets HTTP header / action
hstsMaxAgeSecs number — (not set) Strict-Transport-Security: max-age=<N>
hstsIncludeSubDomains bool true when hstsMaxAgeSecs is set Append ; includeSubDomains to HSTS header
hstsPreload bool false Append ; preload to HSTS header (see hstspreload.org)
csp string — (not set) Content-Security-Policy
xFrameOptions string SAMEORIGIN X-Frame-Options
referrerPolicy string strict-origin-when-cross-origin Referrer-Policy
permissionsPolicy string — (not set) Permissions-Policy — restrict browser feature access
allowedHosts string[] — (not set) Reject requests with a Host header not in this list with 421 Misdirected Request

Always set: X-Content-Type-Options: nosniff and X-XSS-Protection: 1; mode=block are added in both true and object forms — they cannot be disabled.

HSTS and CSP are only set when explicitly configured. HSTS should only be used on HTTPS sites — omit it for HTTP-only configs.

Permissions-Policy restricts access to browser APIs (geolocation, camera, microphone, etc.). See the Permissions Policy spec.

allowedHosts prevents host-header injection attacks where an attacker sends a request with a forged Host header to bypass routing or cache-keying logic.


CORS

# YAML — open CORS (development only)
cors: true

# Locked to specific origins (production)
cors:
  origins: ["https://app.example.com", "https://admin.example.com"]
  credentials: true
  methods: [GET, POST, PUT, DELETE, OPTIONS]
  allowedHeaders: [Authorization, Content-Type, X-Request-ID]
  maxAgeSecs: 86400
// JSON
{
  "cors": {
    "origins": ["https://app.example.com"],
    "credentials": true,
    "methods": ["GET", "POST", "PUT", "DELETE", "OPTIONS"],
    "allowedHeaders": ["Authorization", "Content-Type"],
    "maxAgeSecs": 86400
  }
}
Field Type Default Description
origins string[] ["*"] Allowed origins
methods string[] all Allowed methods
allowedHeaders string[] all Allowed request headers
credentials bool false Allow Authorization / cookies cross-origin
maxAgeSecs number Access-Control-Max-Age (preflight cache)

cors: true allows any origin (*). Always use the object form in production.


IP Filter

Allow or deny requests by client IP or CIDR range. Evaluated before authentication — blocked IPs get 403 immediately.

# YAML — allowlist (deny all others)
ipFilter:
  allow:
    - "10.0.0.0/8"
    - "172.16.0.0/12"
    - "203.0.113.0/24"
  trustProxy: true   # trust X-Forwarded-For for client IP detection

# Denylist
ipFilter:
  deny:
    - "192.0.2.0/24"

# Dry-run mode — log violations without blocking
ipFilter:
  deny:
    - "192.0.2.0/24"
  dryRun: true
// JSON
{
  "ipFilter": {
    "allow": ["10.0.0.0/8", "172.16.0.0/12"],
    "trustProxy": true
  }
}
Field Type Default Description
allow string[] Allowed CIDRs — deny all others
deny string[] Denied CIDRs — allow all others
trustProxy bool false Trust X-Forwarded-For for client IP
dryRun bool false Log blocks without enforcing them — useful for auditing a new deny list before going live

When both allow and deny are set, allow takes precedence.

Dynamic CIDR management: use the Admin API to add or remove deny entries at runtime without a configuration reload — see Admin API — IP deny list.


Error Masking

Replace upstream 5xx bodies with a generic JSON error.

maskErrors: true
// JSON
{ "maskErrors": true }

Clients receive: { "error": "Internal Server Error", "status": 500 }


Upstream Protocol Compatibility

allowDuplicateChunked

By default, Conduit deduplicates Transfer-Encoding: chunked headers from upstream responses. Some misconfigured origins emit Transfer-Encoding: chunked, chunked or two separate Transfer-Encoding: chunked headers, which confuses strict HTTP clients.

allowDuplicateChunked: true   # pass duplicate chunked headers through unmodified

Only enable this for upstreams that deliberately rely on duplicate chunked headers.


Upstream TLS Verification

proxy:
  /api:
    targets: ["https://api-internal:8443"]
    upstreamTls:
      verify: true
      serverName: api-internal.svc.cluster.local
// JSON
{
  "proxy": {
    "/api": {
      "targets": ["https://api-internal:8443"],
      "upstreamTls": {
        "verify": true,
        "serverName": "api-internal.svc.cluster.local"
      }
    }
  }
}
Field Type Default Description
verify bool false Verify upstream cert against system CA store
serverName string from URL Override SNI hostname

mTLS — Client Certificate Authentication

Require clients to present a TLS certificate signed by a trusted CA.

tls:
  cert: /etc/tls/server.crt
  key: /etc/tls/server.key
  clientAuth:
    ca: /etc/tls/client-ca.crt
    optional: false # true = allow connections without cert
// JSON
{
  "tls": {
    "cert": "/etc/tls/server.crt",
    "key": "/etc/tls/server.key",
    "clientAuth": { "ca": "/etc/tls/client-ca.crt", "optional": false }
  }
}
Field Type Default Description
ca path CA PEM file that signs authorized client certs — required
optional bool false false = reject without cert; true = allow without cert

See examples/mtls.yaml


Rhai Script Middleware

Requires cargo build --features rhai

Execute custom Rhai scripts per request. Scripts run in order; any script can reject the request or read headers to make decisions.

→ Full guide with examples: rhai.md

# YAML
middleware:
  - type: script
    path: ./scripts/auth-check.rhai

  - type: script
    path: ./scripts/add-headers.rhai
    config:
      tier: premium
// JSON
{
  "middleware": [
    { "type": "script", "path": "./scripts/auth-check.rhai" },
    {
      "type": "script",
      "path": "./scripts/add-headers.rhai",
      "config": { "tier": "premium" }
    }
  ]
}

Note: inline scripts are not supported — use path to a .rhai file. Optional config is passed to the script as a JSON value.

Available Rhai functions:

Function Description
request.header(name) Read a request header
request.set_header(name, value) Set a request header
request.remove_header(name) Remove a request header
request.uri() Get the request URI
request.method() Get the HTTP method
request.set_response(status, body) Short-circuit with a custom response

WASM Middleware

Requires cargo build --features wasm

→ Full guide with ABI reference, Rust examples, and build instructions: wasm.md

# YAML
middleware:
  - type: wasm
    path: ./plugins/my-plugin.wasm
// JSON
{ "middleware": [{ "type": "wasm", "path": "./plugins/my-plugin.wasm" }] }

Plugins export on_request() -> i32 and a memory export. Return 0 to continue, non-zero to reject. Conduit fails open on errors.

Host functions:

Function Description
conduit_get_header Read a request header
conduit_set_header Set a request header
conduit_remove_header Remove a request header
conduit_get_uri Get request URI
conduit_get_method Get HTTP method
conduit_get_header_names List all header names
conduit_set_response Short-circuit with a custom response
conduit_abort_with_redirect Redirect the client
conduit_get_request_id Get X-Request-ID
conduit_log Write to Conduit log

Connection Pool

Configure the upstream HTTP connection pool per route.

proxy:
  /api:
    targets: ["http://backend:4000"]
    pool:
      maxIdle: 100 # max idle connections to keep open
      idleTimeoutSecs: 90 # close idle connections after 90 s
// JSON
{
  "proxy": {
    "/api": {
      "targets": ["http://backend:4000"],
      "pool": { "maxIdle": 100, "idleTimeoutSecs": 90 }
    }
  }
}
Field Type Default Description
maxIdle number Max idle connections kept alive
idleTimeoutSecs number Close idle connections after this many seconds

Multi-Site

Run multiple virtual hosts from one process.

# YAML
global:
  workers: 4
  backlog: 1024
  shutdownTimeoutSecs: 30
  admin:
    bind: "127.0.0.1:2019"
    token: "$ADMIN_TOKEN"

sites:
  - port: 443
    host: app.example.com
    tls: { cert: ./certs/app.crt, key: ./certs/app.key }
    jwtAuth: { jwksUrl: "https://auth.example.com/.well-known/jwks.json" }
    proxy:
      /api: "http://app-backend:4000"

  - port: 443
    host: admin.example.com
    tls: { cert: ./certs/admin.crt, key: ./certs/admin.key }
    basicAuth: { users: { admin: "$ADMIN_PASS" } }
    proxy:
      /: "http://admin-ui:3000"

  - port: 8080
    static: ./public
    fallback: { file: ./public/404.html, status: 404 }
// JSON
{
  "global": {
    "workers": 4,
    "shutdownTimeoutSecs": 30,
    "admin": { "bind": "127.0.0.1:2019", "token": "$ADMIN_TOKEN" }
  },
  "sites": [
    {
      "port": 443,
      "host": "app.example.com",
      "tls": { "cert": "./certs/app.crt", "key": "./certs/app.key" },
      "proxy": { "/api": "http://app-backend:4000" }
    }
  ]
}

Global field reference

Field Type Default Description
workers number CPU count Worker threads — cold restart to change
backlog number 1024 TCP accept backlog
shutdownTimeoutSecs number Grace period for in-flight requests on shutdown
admin.bind string — (not started) Admin API address. Required to enable the Admin API. Omit to disable it entirely
admin.token string Bearer token required for every Admin API request (strongly recommended)
otlp object OpenTelemetry tracing config (--features otlp required — see OpenTelemetry Tracing)

TCP Proxy

Requires cargo build --features tcp

Forward raw TCP connections without HTTP parsing. Useful for MySQL, PostgreSQL, Redis, SMTP, and any other non-HTTP protocol.

# conduit.yaml
sites:
  - port: 3306
    tcp:
      targets:
        - "mysql-primary:3306"
        - "mysql-replica:3306"
      strategy: round-robin # or "random" (default: round-robin)
      connectTimeoutMs: 5000 # upstream connect timeout (default: 5000)
// conduit.json
{
  "sites": [
    {
      "port": 3306,
      "tcp": {
        "targets": ["mysql-primary:3306", "mysql-replica:3306"],
        "strategy": "round-robin",
        "connectTimeoutMs": 5000
      }
    }
  ]
}
Field Type Default Description
targets string[] Upstream host:port addresses — required
strategy string round-robin "round-robin" or "random"
connectTimeoutMs number 5000 Upstream connect timeout (ms)

Note: tcp cannot be combined with proxy, static, or other HTTP features on the same site. Use a separate port/site for HTTP traffic.


Upload

Requires cargo build --features upload

Enable multipart file upload. The upload handler is only started when this section is present in the config.

# YAML
upload:
  path: /upload # URL path for the upload endpoint (required)
  dir: ./uploads # directory where files are saved (required)
  maxFileSizeBytes: 52428800 # 50 MB per file
  maxTotalSizeBytes: 209715200 # 200 MB total per request
  maxFiles: 10
  allowedMimeTypes: ["image/jpeg", "image/png", "application/pdf"]
  fieldName: file # multipart field name (default: "file")
// JSON
{
  "upload": {
    "path": "/upload",
    "dir": "./uploads",
    "maxFileSizeBytes": 52428800,
    "maxTotalSizeBytes": 209715200,
    "maxFiles": 10,
    "allowedMimeTypes": ["image/jpeg", "image/png", "application/pdf"],
    "fieldName": "file"
  }
}
Field Type Default Description
path string URL path for upload endpoint — required
dir string Directory to save uploaded files — required
maxFileSizeBytes number Max size per individual file
maxTotalSizeBytes number Max total size of all files in one request
maxFiles number Max number of files per request
allowedMimeTypes string[] all Allowed MIME types (e.g. "image/jpeg")
fieldName string "file" Multipart field name to read

Admin API

Configure with global.admin:

global:
  admin:
    bind: "127.0.0.1:2019" # loopback only
    token: "$ADMIN_TOKEN" # optional Bearer token

The Admin API provides 12 endpoints: hot-reload, status, upstream management, cache purge, rate-limit stats, and runtime IP deny-list.

→ Full reference with request/response examples: docs/admin.md


Prometheus Metrics Reference

All metrics are at the metrics.path endpoint.

Metric Type Labels Description
conduit_requests_total counter method, status Total HTTP requests handled
conduit_request_duration_seconds histogram method, status Full request latency (accept → response sent)
conduit_active_connections gauge Requests currently in-flight (site-wide)
conduit_upstream_errors_total counter route, status Upstream 5xx responses per route
conduit_upstream_requests_total counter upstream, status Requests forwarded to each upstream URL
conduit_upstream_latency_seconds histogram upstream Upstream response latency per URL
conduit_upstream_active_connections gauge upstream In-flight requests per upstream URL
conduit_retry_attempts_total counter route, condition Retry attempts by trigger (5xx, connection_error, timeout)
conduit_rate_limit_rejected_total counter site Rate-limited (429) requests per site
conduit_cache_hits_total counter route Proxy cache hits
conduit_cache_misses_total counter route Proxy cache misses
conduit_eventloop_lag_ms gauge Mean scheduling delay (ms) of the admin runtime (--features tokio-metrics)

Example Grafana queries:

# Request rate
rate(conduit_requests_total[5m])

# p99 latency
histogram_quantile(0.99, rate(conduit_request_duration_seconds_bucket[5m]))

# Error rate
rate(conduit_upstream_errors_total[5m])

# Cache hit ratio
rate(conduit_cache_hits_total[5m])
  / (rate(conduit_cache_hits_total[5m]) + rate(conduit_cache_misses_total[5m]))

Clone this wiki locally