Routing
Arc uses a two-layer routing architecture:- Fast path — an allocation-free radix tree (
arc-router) maps a raw URL path to a route ID in the hot path. - Full match — the multi-dimensional router (
arc-core) evaluates host, method, headers, cookies, and query predicates to select the final route.
Path patterns
| Pattern | Example | Behavior |
|---|---|---|
| Exact literal | /api/v1/health | Matches only that exact path |
| Capture | /users/{id}/profile | Captures the id segment |
| Tail capture | /{*rest} | Captures the remainder of the path |
| Wildcard | /static/* | Matches any single segment; does not cross / |
| Deep wildcard | /api/** | Last-segment deep match |
Match predicates
Beyond path, routes can match on:Route priority
Routes are evaluated by specificity:- Exact literal paths beat wildcards
- Longer patterns beat shorter ones
- More specific segment matches beat deeper wildcards
- Within the same specificity, declaration order determines the winner
Request rewriting
Rewrite the URL before forwarding:Header mutations
Add, set, or remove headers on the upstream request:Redirects
Short-circuit a request with a redirect, without touching the upstream:Retry policy
Automatically retry failed upstream requests:Traffic mirroring
Traffic mirroring sends a copy of every request (or a sampled fraction) to a shadow upstream. The main request path is never blocked — mirror tasks are enqueued fire-and-forget.Basic mirror
Advanced mirror with sampling and comparison
Global mirror policy
Configure the memory budget for mirror tasks:max_queue_bytes is exceeded, new mirror tasks are dropped silently. The main request path is unaffected.
How mirroring works internally
- After receiving the upstream response,
submit_allis called with the mirror targets. - For each target: sampling check (lock-free PRNG) → byte reservation (CAS) → non-blocking enqueue.
- If the queue mutex is contended, the task is dropped immediately — the caller never blocks.
- Worker threads pop tasks and execute them: transform request → open a TCP connection → send and receive → compare if enabled.
Connection: close to avoid hanging keep-alive connections.
Metrics
Arc tracks mirror outcomes via Prometheus:| Metric | Description |
|---|---|
arc_mirror_submitted_total | Tasks enqueued |
arc_mirror_sent_total | Tasks successfully forwarded |
arc_mirror_queue_full_total | Tasks dropped (queue full) |
arc_mirror_timeout_total | Tasks dropped (timeout) |
arc_mirror_upstream_error_total | Tasks dropped (upstream error) |
arc_mirror_status_2xx_total | Shadow responses by status class |
arc_mirror_latency_sum_ns | Total shadow response latency |
Traffic splitting
Split traffic across multiple upstreams by weight:Plugins
Arc supports WASM and Rhai plugins that run at defined stages of the request lifecycle.WASM plugins
WASM plugins are compiled WebAssembly modules loaded from disk. Arc uses Wasmtime with epoch-based timeouts to enforce a per-invocation CPU budget:on_request ABI receives the request method and path. The return value determines the outcome:
| Return value | Effect |
|---|---|
0 | Allow the request to proceed |
| Any HTTP status code | Reject with that status |
Rhai scripts
Rhai is a scripting language that runs inline scripts with a configurable operation count limit:Attaching plugins to routes
Plugins run at the configured pipeline stage:| Stage | When |
|---|---|
l4 | Before HTTP parsing, at the TCP level |
request_headers | After request headers are received (most common) |
request_body | After request body is buffered |
response_headers | After upstream response headers arrive |
response_body | After response body is received |
log | At access log emission time |
Plugin isolation
Each worker has its own Wasmtime instance pool. Plugin instances are not shared across workers. If a WASM module exceeds itsbudget, the invocation is interrupted and the request is rejected with 500. Panics inside plugins are caught and counted.
Troubleshooting
Route is not matching when expected
Route is not matching when expected
Arc evaluates routes by specificity: exact path wins over wildcard; longer patterns win over shorter. If two routes both match, the more specific one wins. Add a
name to each route and check arc_route_requests_total{route="..."} in /metrics to see which route is actually matching. Use GET /v1/config on the control plane to inspect the compiled route order.Path parameters are not being extracted
Path parameters are not being extracted
Ensure the path pattern uses
{param} syntax (e.g., /users/{id}). The captured value is available as $param in rewrite rules. Wildcard * does not capture — use {*rest} for tail capture.Rate limit is triggering too aggressively
Rate limit is triggering too aggressively
Check the
key field in rate_limit. The default client_ip means each client IP gets its own bucket. If you need a global limit across all clients, use key: { by: route } (inline) or the multi-line form key:\n by: route. Adjust burst to allow short traffic spikes.Traffic mirror is dropping requests
Traffic mirror is dropping requests
Check
arc_mirror_queue_full_total in /metrics. If this is incrementing, the in-memory mirror queue (max_queue_bytes, default 50 MB) is full. Reduce mirror sample rate or increase max_queue_bytes. If arc_mirror_upstream_error_total is incrementing, the mirror upstream is unreachable.Traffic split is not distributing as expected
Traffic split is not distributing as expected
Weights are relative, not percentages.
[{upstream: a, weight: 1}, {upstream: b, weight: 3}] means 25% to a and 75% to b. The splitter uses a per-worker PRNG — distribution converges over many requests but may appear uneven on small samples.WASM plugin is timing out
WASM plugin is timing out
Each WASM plugin execution is bounded by
budget. Increase the budget in plugins.wasm[].budget if the plugin legitimately needs more time. Check arc_plugin_timeout_total in /metrics to confirm timeouts are occurring.
