Routing strategies¶
Configure with routing.strategy in your TOML config. The default is best_score.
best_score (default)¶
Reads go to the provider with the highest current health score. The full list sorted by score descending is used as the failover order.
Good for: most production workloads. Always prefers the most reliable provider at the moment.
weighted_random¶
Each provider's selection probability is proportional to config_weight × health_score. The remaining providers sorted by score serve as the failover list.
[routing]
strategy = "weighted_random"
[[providers]]
name = "helius"
url = "..."
weight = 2 # twice as likely to be selected
[[providers]]
name = "quicknode"
url = "..."
weight = 1
Good for: distributing load across providers of similar quality, staying within per-provider rate limits, spreading costs.
failover_ordered¶
Providers are tried in the order they appear in the config file. Circuit-open providers are skipped. The first provider in config is always primary until its circuit opens.
[routing]
strategy = "failover_ordered"
[[providers]]
name = "primary" # always first
url = "..."
[[providers]]
name = "backup" # used only when primary is unavailable
url = "..."
Good for: clear primary/secondary hierarchy, deterministic routing, cost control (cheap primary, expensive failover).
Example — dedicated node with metered backup:
[health]
interval_ms = 500 # detect failure fast
circuit_open_failures = 3 # open after ~1.5 s of failures
circuit_cooldown_secs = 15
[routing]
strategy = "failover_ordered"
max_retries = 1
# broadcast_writes = false # default — don't charge the metered provider for writes
[[providers]]
name = "dedicated"
url = "${DEDICATED_URL}"
[[providers]]
name = "quicknode-backup"
url = "https://your-endpoint.quiknode.pro/${QUICKNODE_API_KEY}"
The metered provider only receives traffic when the primary circuit is open. See examples/dedicated-with-metered-backup.toml for the full config.
parallel_race¶
The request is sent to all healthy providers simultaneously. The first successful response is returned to the client immediately — your latency tracks the fastest provider, not the slowest. The remaining in-flight requests finish in the background so every provider's health and metrics are still recorded.
Good for: latency-critical reads where you can absorb N× the provider traffic cost.
Retries¶
On a retryable error, the proxy tries the next provider in the ordered list up to routing.max_retries times (default: 2).
| Category | Code | Description | Behaviour |
|---|---|---|---|
| Retryable (HTTP) | 429 | Rate-limited | Try next provider |
| Retryable (HTTP) | 500, 502, 503, 504 | Server / gateway error | Try next provider |
| Retryable (RPC) | -32003 | Node unhealthy or not caught up — the provider rejected the request because its slot lag is too high or it failed a preflight simulation | Try next provider |
| Retryable (RPC) | -32005 | Node is behind — provider explicitly signals its slot is stale | Try next provider |
| Retryable (RPC) | -32603 | Internal error — transient fault on the provider side | Try next provider |
| Non-retryable (HTTP) | 400, 401, 403, 404 | Client error (bad request, auth, not found) | Return immediately |
| Non-retryable (RPC) | -32700 | Parse error — malformed JSON | Return immediately |
| Non-retryable (RPC) | -32600 | Invalid request object | Return immediately |
| Non-retryable (RPC) | -32601 | Method not found | Return immediately |
| Non-retryable (RPC) | -32602 | Invalid method params | Return immediately |
Write broadcasting (optional)
Set routing.broadcast_writes = true to send every method in routing.write_methods (default: sendTransaction) to all healthy providers simultaneously and return the fastest success. Duplicate submissions are harmless — Solana deduplicates by signature. Off by default.
Customising the write list
routing.write_methods controls which methods take the write path. It defaults to ["sendTransaction"]. simulateTransaction is not a write by default — it's read-only (no on-chain mutation) and isn't in the transaction-landing path, so it routes like an ordinary read (and can be raced with parallel_race). If you specifically want it broadcast, add it: write_methods = ["sendTransaction", "simulateTransaction"].
Submission-only providers¶
Some endpoints — transaction-landing services like circular.fi or staked-connection gateways — only accept sendTransaction and don't serve general reads. Scope them with a per-provider methods allowlist so they only receive the calls they support:
[[providers]]
name = "helius"
url = "https://mainnet.helius-rpc.com/?api-key=${HELIUS_API_KEY}"
[[providers]]
name = "landing-service"
url = "https://fast.example/api/submit"
methods = ["sendTransaction"] # only ever receives sendTransaction
Reads route only to providers that support them, so landing-service above never receives a getAccountInfo it would reject. With broadcast_writes = true, a sendTransaction fans out across every provider that lists it — your general RPC plus the landing service — and the fastest valid success wins.
Because a submission-only provider can't answer the getSlot health probe, the proxy skips probing it and scores its health from live request outcomes instead (its circuit still opens on repeated failures). Pair this with broadcast_writes = true for a "land transactions fast" setup — see examples/fast-tx-landing.toml.