Configuration Reference¶
RPC Plane is configured via a TOML file (default: rpc-plane.toml in the current directory).
Run rpc-plane init to generate a starter file. Use -c /path/to/config.toml to specify a different path.
Environment variable interpolation: any value can reference env vars:
url = "https://mainnet.helius-rpc.com/?api-key=${HELIUS_API_KEY}"
url = "https://endpoint.quiknode.pro/$QUICKNODE_KEY" # braces optional
Unset variables expand to an empty string and the proxy logs a warning naming each one — a typo like ${HELIUS_API_KY} therefore surfaces at startup instead of as silent 401s. If you hardcode the full URL and token instead of using a variable, nothing is referenced and no warning is emitted. rpc-plane check reports the same unset-variable warnings before testing connectivity.
The proxy also validates provider entries on startup: provider names must be unique (a duplicate is a hard load error) and every url must be non-empty.
[server]¶
[server]
listen = "127.0.0.1:9400" # address for the proxy (JSON-RPC endpoint)
metrics_listen = "127.0.0.1:9401" # address for Prometheus metrics + /health
# listen_backlog = 4096 # OS TCP listen backlog (default 4096)
# pool_max_idle_per_host = 512 # idle outbound connections per provider (default 512)
# worker_threads = 4 # Tokio worker threads (default: number of logical CPUs)
| Key | Default | Description |
|---|---|---|
listen |
"127.0.0.1:9400" |
TCP address the proxy listens on. Change to 0.0.0.0 to expose on all interfaces. |
metrics_listen |
"127.0.0.1:9401" |
Metrics and health endpoint. Serves GET /metrics (Prometheus) and GET /health (JSON). |
listen_backlog |
4096 |
OS TCP listen backlog for both sockets. Raise net.core.somaxconn to match (sysctl -w net.core.somaxconn=4096). |
pool_max_idle_per_host |
512 |
Max idle outbound connections kept per provider. Set to at least your expected peak concurrency to avoid cold TCP handshakes under load. |
worker_threads |
number of logical CPUs | Number of Tokio runtime worker threads. Set to a fixed count to dedicate cores when sharing a host with other services. Requires a restart to change. |
Note
server.listen, metrics_listen, and listen_backlog require a restart to change — they are not hot-reloaded.
[health]¶
Controls how providers are probed and scored.
[health]
interval_ms = 1000 # probe each provider this often
window_secs = 60 # sliding window for error rate
slot_drift_threshold = 10 # slots behind tip → drifting
# reference_url = "https://api.mainnet-beta.solana.com" # optional external checkpoint
circuit_open_failures = 5 # consecutive failures → circuit open
circuit_error_threshold = 0.5 # error rate threshold → circuit open
circuit_cooldown_secs = 30 # wait before half-open probe
# Score weights (auto-normalised, do not need to sum to 1)
w_latency = 0.4
w_error = 0.3
w_slot = 0.2
w_success = 0.1
Probe settings¶
| Key | Default | Description |
|---|---|---|
interval_ms |
1000 |
How often (ms) to send a getSlot probe to each provider. |
window_secs |
60 |
Sliding window (seconds) over which error rate is computed. |
slot_drift_threshold |
10 |
Slots behind network tip before slot freshness score drops to 0. |
reference_url |
(none) | Optional external endpoint probed for its slot but never routed to. Its slot is folded into the network tip so a stall shared by every provider — or a single-provider / own-node deploy that is otherwise its own tip — still surfaces as drift. Default-off: leaving it unset keeps the proxy's promise of zero outbound connections beyond your configured providers. Point it at the matching cluster — a mainnet reference against a devnet provider set (or vice versa) reports false drift. See External slot reference. |
Circuit breaker¶
| Key | Default | Description |
|---|---|---|
circuit_open_failures |
5 |
Consecutive probe failures that trip the circuit open. |
circuit_error_threshold |
0.5 |
Rolling error rate threshold (0.0–1.0) that opens the circuit. |
circuit_cooldown_secs |
30 |
Seconds before attempting a half-open probe after the circuit opens. |
Health score weights¶
| Key | Default | What it controls |
|---|---|---|
w_latency |
0.4 |
Round-trip latency component |
w_error |
0.3 |
Error rate component |
w_slot |
0.2 |
Slot freshness component |
w_success |
0.1 |
Recent probe success rate |
See Health scoring for the full formula.
[routing]¶
[routing]
strategy = "best_score" # routing strategy for reads
max_retries = 2 # retries on transient errors
cost_aware = false # enable cost_efficiency_score weight
cost_weight = 0.2 # w_cost when cost_aware = true
broadcast_writes = false # fan out write methods to all providers
write_methods = ["sendTransaction"] # methods routed as writes
| Key | Default | Description |
|---|---|---|
strategy |
"best_score" |
Read routing strategy. See Routing strategies. |
max_retries |
2 |
Maximum provider retries on retryable errors (HTTP 429/5xx, RPC -32003/-32005/-32603). |
cost_aware |
false |
When true, adds a cost efficiency component to the score. Requires [providers.pricing]. |
cost_weight |
0.2 |
Weight of the cost efficiency score when cost_aware = true. |
broadcast_writes |
false |
When true, sends every method in write_methods to all healthy providers simultaneously. Maximises transaction landing probability at the cost of N× provider traffic. |
write_methods |
["sendTransaction"] |
Methods classified as writes. With broadcast_writes = true they fan out to all providers; otherwise they route sequentially through the failover list. simulateTransaction is not a write by default (it's read-only and not in the landing path) — add it here to broadcast it too. |
[[providers]]¶
At least one provider is required. Add multiple [[providers]] blocks for multi-provider routing.
[[providers]]
name = "helius"
url = "https://mainnet.helius-rpc.com/?api-key=${HELIUS_API_KEY}"
weight = 1
[[providers]]
name = "quicknode"
url = "https://your-endpoint.quiknode.pro/${QUICKNODE_API_KEY}"
weight = 1
# http3 = true # opt-in QUIC — see "When to enable HTTP/3" below
| Key | Required | Default | Description |
|---|---|---|---|
name |
Yes | — | Unique identifier. Must be unique across providers (a duplicate is a load error). Used in logs and metric labels. |
url |
Yes | — | Full HTTP URL including API key. Supports env var interpolation. |
weight |
No | 1 |
Relative weight for weighted_random strategy. No effect on other strategies. |
http3 |
No | false |
Use HTTP/3 (QUIC) for outbound connections to this provider. No HTTP/2 fallback: if QUIC can't be established the request fails over to the next provider — it does not silently downgrade to HTTP/2 — so only enable it for providers you've confirmed serve QUIC. Experimental: depends on a pinned reqwest built with the unstable HTTP/3 feature; treat as best-effort until upstream stabilizes it. See When to enable HTTP/3. |
methods |
No | (all) | Restrict this provider to a set of JSON-RPC methods. Omit to serve every method. Use it to add a submission-only endpoint (e.g. a transaction-landing service) — methods = ["sendTransaction"] — so it never receives reads it can't answer. Providers that exclude getSlot skip the health probe and are scored from live request outcomes. See Submission-only providers. |
max_rps |
No | (unlimited) | Optional per-provider rate cap in requests/second. Once the provider is dispatching at its cap it is treated as unavailable so load sheds to peers with headroom (exactly like a demoted health score). A load-shedding hint, not a hard throttle — if every eligible provider is at its cap the proxy still forwards the request (degraded) rather than failing it, so a single at-capacity provider never self-inflicts a 502. Omit for no limit (zero per-request overhead). Hot-reloadable. See Per-provider rate limiting. |
When to enable HTTP/3¶
HTTP/3 (QUIC) is off by default, and most deployments should leave it off. RPC Plane keeps a warm, pooled connection to each provider, and on a warm connection over a low-latency, low-loss network HTTP/3 and HTTP/2 perform identically. In a datacenter — where your app and the proxy sit close to well-connected providers — enabling it buys nothing measurable, and QUIC's userspace stack can cost slightly more CPU than the kernel's TCP.
HTTP/3 earns its keep on the cold and lossy paths, where its 1-RTT handshake and lack of TCP head-of-line blocking matter:
- Connection churn — cold starts, failover storms, or providers that drop idle connections — where you keep re-paying the setup cost. QUIC establishes in ~1 RTT versus TCP+TLS's ~3, so a fresh connection is dramatically cheaper.
- High-latency or lossy links — cross-region hops, congested networks, or a residential/last-mile connection — where a single lost packet stalls every in-flight request on an HTTP/2 (TCP) connection but not on HTTP/3.
Enable it per provider only after confirming the endpoint serves QUIC. There is no HTTP/2 fallback (see the table above): if the QUIC connection fails, requests to that provider fail and fail over to another provider.
Per-provider rate limiting¶
Set max_rps on a provider to cap how fast RPC Plane dispatches to it — useful for staying under a free-tier or metered plan's documented per-second limit, or for keeping a metered backup's spend bounded.
[[providers]]
name = "free-key"
url = "https://mainnet.helius-rpc.com/?api-key=${HELIUS_API_KEY}"
max_rps = 10 # set to your plan's documented per-second limit
Each capped provider gets a token bucket that holds one second of tokens (max_rps) and refills continuously at max_rps/second — so it can absorb a short burst up to max_rps and then sustains max_rps. A request spends one token when it is dispatched. When the bucket is empty the provider is marked rate-limited and treated as unavailable, so the router shifts new requests to peers that still have headroom — the same way it routes around a demoted health score or an open circuit.
Two things are deliberately not how it works:
- It is a load-shedding hint, not a hard throttle. If every provider that can serve a method is at its cap, RPC Plane forwards the request anyway (the degraded fallback) rather than returning an error. This means a single at-capacity provider — or a single-provider deployment — never rejects traffic on its own; the cap only matters when there is a peer with headroom to shed to. If you truly need to bound one provider's throughput, give the fleet enough total capacity across providers that the sum of caps exceeds your offered load.
- It is per-provider, not per-method. All methods draw from the same bucket. Per-method limits are out of scope.
A rate-limited provider is visible on the /health endpoint ("rate_limited": true, "available": false) and each shed request increments the rpc_plane_rate_limited_total{provider} metric. max_rps is hot-reloadable: changing it updates the bucket in place without disturbing the provider's health state or forcing a client rebuild.
Note that max_rps is distinct from an upstream HTTP 429: a 429 is the provider telling you it's throttling (RPC Plane demotes that provider's score but keeps its circuit closed — see Health scoring), whereas max_rps is a limit you configure so the proxy stops sending before the provider has to say no.
[providers.pricing]¶
Optional cost tracking. Enables cost-aware routing when routing.cost_aware = true.
[[providers]]
name = "helius"
url = "https://mainnet.helius-rpc.com/?api-key=${HELIUS_API_KEY}"
[providers.pricing]
model = "credits"
monthly_budget_usd = 200
| Key | Description |
|---|---|
model |
Pricing model: "credits", "compute_units", or "flat". |
monthly_budget_usd |
Monthly budget cap in USD. |
[reporting]¶
Sends telemetry to a remote endpoint. Absent by default — the proxy runs with no outbound connections.
[reporting]
endpoint = "http://localhost:3000/api/ingest"
api_key = "rp_live_xxxx"
flush_interval_ms = 60000
| Key | Description |
|---|---|
endpoint |
HTTP endpoint to POST telemetry batches to. |
api_key |
Bearer token sent with each batch. |
flush_interval_ms |
Aggregation window and POST interval (ms). Minimum 10000. Default 60000. |
Full example¶
[server]
listen = "127.0.0.1:9400"
metrics_listen = "127.0.0.1:9401"
[health]
interval_ms = 1000
window_secs = 60
slot_drift_threshold = 10
circuit_open_failures = 5
circuit_error_threshold = 0.5
circuit_cooldown_secs = 30
w_latency = 0.4
w_error = 0.3
w_slot = 0.2
w_success = 0.1
[routing]
strategy = "best_score"
max_retries = 2
[[providers]]
name = "helius"
url = "https://mainnet.helius-rpc.com/?api-key=${HELIUS_API_KEY}"
weight = 1
[[providers]]
name = "quicknode"
url = "https://your-endpoint.quiknode.pro/${QUICKNODE_API_KEY}"
weight = 1
[[providers]]
name = "triton"
url = "https://your-pool.rpcpool.com/${TRITON_API_KEY}"
weight = 1