Skip to main content
Unknown fields cause a deserialization error because #[serde(deny_unknown_fields)] is applied throughout. Durations use humantime format: "30s", "500ms", "2m", etc.

Top-level structure

io_uring:       # io_uring ring sizing and SQPOLL (restart required to change)
node:           # runtime capacity settings
listeners:      # bind addresses and protocols
upstreams:      # named backend groups
routes:         # request routing rules
plugins:        # WASM and Rhai plugin registry
observability:  # metrics server
logging:        # access log output, sampling, redaction
control_plane:  # node-local management API

node

Controls worker count, connection limits, and global I/O timeouts.
FieldTypeDefaultDescription
idstring"arc-node"Human-readable node identifier
workersinteger0Data-plane worker threads. 0 = auto-detect CPU count
max_connectionsinteger0Hard limit on accepted TCP connections. 0 = unlimited
read_timeoutduration30sDownstream read timeout
write_timeoutduration30sDownstream write timeout
idle_timeoutduration60sKeep-alive idle timeout
node:
  id: "arc-prod-1"
  workers: 0
  max_connections: 100000
  read_timeout: 30s
  write_timeout: 30s
  idle_timeout: 60s

io_uring

Controls the io_uring ring parameters for each worker thread. All fields require a process restart to change.
FieldTypeDefaultDescription
uring_entriesinteger2048Submission queue ring size (power of 2). Increase if arc_ring_sq_dropped_total is non-zero.
sqpollbooleanfalseEnable SQPOLL (kernel polling thread). Eliminates all submission syscalls. Requires CAP_SYS_ADMIN on kernels < 5.11. Recommended for bare-metal.
sqpoll_idle_msinteger2000Milliseconds before the SQPOLL kernel thread sleeps when idle.
If arc_ring_sq_dropped_total or arc_ring_cq_overflow_total are non-zero, the ring is too small. Double uring_entries.
io_uring:
  uring_entries: 4096   # 4096 for high-traffic nodes
  sqpoll: true          # recommended for bare-metal
  sqpoll_idle_ms: 2000

listeners

An array of listener entries. Each entry binds one address.
FieldTypeRequiredDescription
namestringyesIdentifier, referenced by ACME config
kindstringyesProtocol type (see below)
bindstringyesSocket address, e.g. "0.0.0.0:443"
socketobjectnoOS socket tuning
tlsobjectnoRequired for https and h3 kinds
Listener kinds:
ValueDescription
httpHTTP/1.1 and H2C cleartext
httpsHTTPS with ALPN (HTTP/1.1 + HTTP/2). Requires tls
h3HTTP/3 over QUIC. Requires tls
tcpRaw TCP L4 proxy
udpRaw UDP L4 proxy
socket options:
FieldTypeDefaultDescription
so_reuseportbooleanfalseEnable SO_REUSEPORT
tcp_fastopen_backlogintegernullEnable TCP Fast Open
dscpintegernullDSCP/TOS byte for QoS
keepaliveobjectnullTCP keepalive probes (idle, interval, count)
listeners:
  - name: http
    kind: http
    bind: "0.0.0.0:8080"
    socket:
      so_reuseport: true

  - name: https
    kind: https
    bind: "0.0.0.0:8443"
    tls:
      certificates:
        - sni: "example.com"
          cert_pem: "./certs/example.com.crt"
          key_pem: "./certs/example.com.key"

tls

Active only when listener kind is https or h3.
FieldTypeDefaultDescription
acmeobjectnullACME automatic certificate management
certificatesarray[]Static certificates loaded at startup
min_versionstringnulltls12 or tls13
max_versionstringnullMaximum TLS version
cipher_suitesstring[][]Cipher suite preference list (empty = Rustls defaults)
session_resumptionbooleantrueEnable TLS session resumption
certificates entries:
FieldDescription
sniHostname. Exact match or wildcard ("*.example.com")
cert_pemPath to PEM certificate chain
key_pemPath to PEM private key
The first certificate in the array is the default when no SNI matches.

tls.acme

Enables automatic certificate issuance and renewal from an ACME CA.
FieldTypeDefaultDescription
emailstringnullContact email for the ACME account
directory_urlstringLet’s EncryptACME directory endpoint
account_keyobjectrequiredKey configuration
domainsstring[]requiredHostnames to obtain certificates for
challengeobjectrequiredChallenge method
renew_beforeduration720hRenewal lead time before expiry
account_key:
FieldDescription
algorithmed25519 or rsa2048
encrypted_key_pathPath to encrypted key file
passphrase{ type: env, name: VAR } or { type: file, path: /path }
challenge types:
typeExtra fieldsDescription
http_01listenerHTTP-01 via named listener (must be port 80)
tls_alpn_01listenerTLS-ALPN-01 via named TLS listener
dns_01providerDNS-01 via Cloudflare, Route53, RFC2136, webhook, or hook
Full ACME configuration example:
tls:
  acme:
    email: you@example.com
    domains: [example.com, www.example.com]
    account_key:
      algorithm: ed25519
      encrypted_key_path: /etc/arc/acme-key.enc
      passphrase:
        type: env
        name: ACME_KEY_PASSPHRASE
    challenge:
      type: tls_alpn_01
      listener: https
    renew_before: 720h

upstreams

An array of named backend groups. Routes reference upstreams by name.
FieldTypeDefaultDescription
namestringrequiredIdentifier referenced in route actions
discoveryobjectrequiredHow endpoints are found
lbobjectpeak_ewmaLoad balancing algorithm
healthobject(empty)Active and passive health checks
poolobjectsee belowConnection pool settings
timeoutsobjectsee belowPer-upstream timeout overrides

discovery

Static endpoints:
discovery:
  type: static
  endpoints:
    - address: "10.0.0.1:3000"
      weight: 1
    - address: "10.0.0.2:3000"
      weight: 2
DNS-based discovery:
discovery:
  type: dns
  hostname: api.internal
  port: 3000
  respect_ttl: true
  fallback_poll: 10s

lb — load balancing

algorithmExtra fieldsDescription
round_robinUniform distribution
weighted_round_robinWeighted by endpoint.weight
least_requestsFewest in-flight requests
consistent_hashkey, virtual_nodesKetama consistent hashing
peak_ewmadecay (duration)Peak EWMA latency-based selection
Consistent hash key sources: client_ip, header (with name), or cookie (with name).

health

Active health checks (proactive polling):
health:
  active:
    interval: 10s
    path: /health
    fail_after: 3
    pass_after: 2
Passive health checks (outlier ejection):
health:
  passive:
    error_rate_threshold: 0.2
    window: 30s
    ejection_time: 60s

pool — connection pool

FieldDefaultDescription
max_idle1024Maximum idle connections per upstream
idle_ttl30sEvict idle connections older than this
max_lifetime300sMaximum total connection lifetime

timeouts — upstream timeouts

FieldDefaultDescription
connect2sTCP connection establishment timeout
write30sTimeout writing request to upstream
ttfb5sTime-to-first-byte after request sent
read30sFull response read timeout

tls — upstream TLS

Optional. When present, Arc opens TLS connections to the upstream endpoints.
FieldDefaultDescription
ca_pemnullCA certificate path for verifying the upstream’s TLS certificate
client_cert_pemnullClient certificate path (for mutual TLS)
client_key_pemnullClient private key path (for mutual TLS)
insecurefalseSkip server certificate verification. Use only in development.
See TLS & Certificates for full upstream TLS examples.

routes

An array of route rules. Routes are evaluated by specificity — exact matches win over wildcards; longer patterns win over shorter ones.
FieldTypeDefaultDescription
namestringrequiredRoute identifier (used in logs and plugins)
matchobjectrequiredMatch predicates
actionobjectrequiredWhat to do when matched
rate_limitobjectnullPer-route rate limiting
mirrorobjectnullTraffic mirroring
splitobjectnullWeighted traffic split
pluginsarray[]Plugins attached to this route

match

FieldDefaultDescription
host[]Hostname(s). Empty = any host
methods[]HTTP methods. Empty = any method
pathrequiredPath pattern
headers[]Header predicates (all must match)
cookies[]Cookie predicates
query[]Query parameter predicates
exprnullBoolean expression combining named matchers (AND/OR/NOT)
Path patterns:
PatternExampleBehavior
Exact/api/v1/usersExact match only
Capture/users/{id}Captures a path segment
Tail capture/{*rest}Captures the remainder
Wildcard/static/*Any single segment
Header predicates (op field): exists, contains, regex, equals. All require name; value-based ops also require value or pattern.

action

FieldDefaultDescription
upstreamrequiredName of the upstream group
rewritenullURL rewrite (pattern + replace)
headers[]Header mutations before forwarding
redirectnullShort-circuit redirect (status + location)
retrysee belowRetry policy
Header mutations (op field): add, set, remove. Retry policy:
FieldDefaultDescription
max_retries1Maximum retry attempts
backoff50msFixed delay between retries
idempotent_onlytrueOnly retry GET, HEAD, OPTIONS, etc.

rate_limit

FieldDefaultDescription
qpsrequiredToken bucket refill rate
burstrequiredToken bucket capacity (max burst)
keyclient_ipKeying strategy
status429HTTP status when limit exceeded
Key strategies: client_ip, header (with name), or route (single shared limit).

mirror

# Shorthand
mirror: api-shadow

# Full form with options
mirror:
  - upstream: api-v2-shadow
    sample: 0.5
    timeout: 3s
    transform:
      headers:
        set: { X-Shadow: "true" }
        remove: [X-Real-User]
      path: "/v2$path"
    compare:
      enabled: true
      ignore_headers: [Date]
      ignore_body_fields: ["$.timestamp"]
      on_diff: log

split — traffic split

split:
  choices:
    - upstream: app-v1
      weight: 90
    - upstream: app-v2
      weight: 10
  key:
    source: client_ip

plugins

Global plugin registry. Routes reference plugins by name.
plugins:
  wasm:
    - name: my-filter
      file: ./plugins/my-filter.wasm
      budget: 2ms

  rhai:
    - name: add-header
      inline: |
        request.set_header("X-Via", "arc");
        0
      max_ops: 50000

WASM plugin fields

FieldDefaultDescription
namerequiredPlugin identifier
filerequiredPath to .wasm binary
budget2msCPU time budget per invocation

Rhai script fields

FieldDefaultDescription
namerequiredScript identifier
inlinerequiredInline Rhai source code
max_ops50000Operation count limit per invocation

Attaching plugins to a route

routes:
  - name: api
    match:
      path: /api/{*rest}
    action:
      upstream: app
    plugins:
      - name: my-filter
        stage: request_headers
Plugin stages:
ValueDescription
l4L4 TCP layer, before HTTP parsing
request_headersAfter request headers are received
request_bodyAfter request body is buffered
response_headersAfter upstream response headers arrive
response_bodyAfter response body is received
logAt access log emission time

observability

Controls the metrics and admin server. See also logging below for access log configuration.
FieldDefaultDescription
metrics_bind"127.0.0.1:9090"Prometheus /metrics bind address
metrics_enabledtrueEnable the metrics endpoint
tracingnullOTLP tracing export (endpoint, insecure)
observability:
  metrics_bind: "0.0.0.0:9090"   # expose to all interfaces (protect with firewall)
  metrics_enabled: true
  tracing:
    endpoint: "http://otel-collector:4317"
    insecure: true

logging

Controls the structured NDJSON access log. logging is a top-level key, separate from observability. logging.output:
FieldDefaultDescription
logging.output.file/var/log/arc/access.logOutput file path
logging.output.stdoutfalseAlso write to stdout
logging.output.rotation.max_size500mbRotate when the file reaches this size
logging.output.rotation.max_files30Number of rotated files to keep
logging.output.rotation.compresstrueGzip compress rotated files
logging.access:
FieldDefaultDescription
logging.access.sample0.01Fraction of requests to log (0.0–1.0). This is the main sampling control.
logging.access.force_on_status[401,403,429,500,502,503,504]Always log these status codes, regardless of sample
logging.access.force_on_slow500Always log requests slower than this many milliseconds, regardless of sample
observability.access_log.sample (default 1.0) and logging.access.sample (default 0.01) are two separate fields. observability.access_log.sample is an older knob; logging.access.sample overrides it. Use logging.access.sample for all new configuration.
logging.redact:
FieldDefaultDescription
logging.redact.headers["Authorization","Cookie",…]Headers replaced with [REDACTED]
logging.redact.query_params["token","secret",…]Query params replaced with [REDACTED]
logging.writer:
FieldDefaultDescription
logging.writer.ring_capacity8192SPSC ring buffer size per worker (entries)
logging.writer.batch_bytes262144Flush when batch reaches this size (bytes)
logging.writer.flush_interval50Time-based flush interval (milliseconds)
logging:
  output:
    file: /var/log/arc/access.log
    stdout: false
    rotation:
      max_size: 500mb
      max_files: 30
      compress: true
  access:
    sample: 0.01
    force_on_status: [401, 403, 429, 500, 502, 503, 504]
    force_on_slow: 500
  redact:
    headers: [Authorization, Cookie, X-Api-Key]
    query_params: [token, secret, password]

control_plane

FieldDefaultDescription
enabledfalseMaster switch. The server does not start unless true.
bind"127.0.0.1:22100"Control plane HTTP server listen address
rolestandalonestandalone, leader, or follower
node_idhostnameUnique node name for cluster gossip
peers[]Peer base URLs for leader-initiated HTTP push
quorum0Minimum nodes that must accept config before commit. 0 = majority
auth_tokennullBearer token. null = loopback-only, no auth
pull_fromnullLeader URL for follower long-poll
longpoll_timeout_ms30000Server-side hold duration for long-poll requests
peer_timeout_ms5000HTTP timeout for peer-to-peer validate/commit calls
peer_concurrency16Max parallel peer calls during cluster config push
runtime_threads2Tokio worker thread count for the control-plane runtime
compile_threads2spawn_blocking thread cap for JSON compile work
max_body_bytes16 MiBMaximum request body size for config endpoints
All control_plane fields require a process restart to change. See Gateway control plane API reference for endpoint documentation.

Complete example

node:
  id: "prod-1"
  workers: 0
  max_connections: 100000
  read_timeout: 30s
  write_timeout: 30s
  idle_timeout: 60s

listeners:
  - name: http
    kind: http
    bind: "0.0.0.0:8080"
  - name: https
    kind: https
    bind: "0.0.0.0:8443"
    tls:
      certificates:
        - sni: "*.example.com"
          cert_pem: /etc/arc/certs/wildcard.crt
          key_pem: /etc/arc/certs/wildcard.key

upstreams:
  - name: api
    discovery:
      type: static
      endpoints:
        - address: "10.0.1.1:3000"
        - address: "10.0.1.2:3000"
    lb:
      algorithm: peak_ewma
      decay: 10s
    health:
      active:
        interval: 10s
        path: /health
    pool:
      max_idle: 512
      idle_ttl: 30s
    timeouts:
      connect: 2s
      ttfb: 5s

routes:
  - name: api
    match:
      path: /api/{*rest}
      methods: [GET, POST, PUT, DELETE]
    action:
      upstream: api
      retry:
        max_retries: 2
        backoff: 100ms
        idempotent_only: true
    rate_limit:
      qps: 1000
      burst: 2000
      key:
        by: client_ip

  - name: root
    match:
      path: /{*rest}
    action:
      upstream: api

observability:
  metrics_bind: "127.0.0.1:9090"

control_plane:
  enabled: true
  bind: "127.0.0.1:22100"
  auth_token: "your-secret-token"