Turbine
Multi-chain RPC proxy with intelligent endpoint rotation. Unlike EVM-only proxies, Turbine works with any blockchain that speaks JSON-RPC over HTTP.
How It Works
Client (HTTP or WebSocket)
│
│ POST /ethereum ─── by chain name
│ POST /1 ─── by chain ID
│ GET /ethereum (WS) ─── WebSocket upgrade
│
▼
┌──────────────────────────────────────────────────────────────────┐
│ TURBINE │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 1. API KEY AUTH │ │
│ │ Validate Authorization: Bearer <key> or X-Api-Key │ │
│ │ Per-key rate limit check ──── 401 / 429 if invalid │ │
│ └──────────────────────┬──────────────────────────────────────┘ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 2. ROUTE RESOLVE │ │
│ │ Match path name (/ethereum) or chain ID (/1, /8453) │ │
│ └──────────────────────┬──────────────────────────────────────┘ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 3. CHAIN RATE LIMIT │ │
│ │ Token bucket per chain ──── 429 if exceeded │ │
│ └──────────────────────┬──────────────────────────────────────┘ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 4. CACHE LOOKUP │ │
│ │ Per-method TTL cache (EVM / Solana presets) │ │
│ │ Hit? ── return cached ──────────────────────── response │ │
│ └──────────────────────┬──────────────────────────────────────┘ │
│ miss ▼ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 5. ENDPOINT POOL │ │
│ │ │ │
│ │ Rotation Strategy Method Routing │ │
│ │ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄┄ ┄┄┄┄┄┄┄┄┄┄┄┄┄┄ │ │
│ │ • round-robin Endpoints declare which │ │
│ │ • weighted methods they accept │ │
│ │ • latency-based (e.g. eth_sendRawTransaction │ │
│ │ → private mempool endpoint) │ │
│ │ Endpoints │ │
│ │ A ✓ B ✓ C ✗ D ✓ E ✓ │ │
│ └──────────┬──────────────────────────────────────────────────┘ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ 6. FORWARD + HEDGE │ │
│ │ │ │
│ │ Primary request ─────────────────────────┐ │ │
│ │ │ │ │
│ │ After delay_ms, fire hedge ──┐ │ │ │
│ │ to a different endpoint │ first │ │ │
│ │ │ success │ │ │
│ │ Inject upstream auth: │ wins │ │ │
│ │ Basic / Bearer / Header │ ┌─────┘ │ │
│ │ │ │ │ │
│ │ Fail? ── retry with next └─────┤ │ │
│ │ healthy endpoint (up to │ │ │
│ │ max_retries attempts) ▼ │ │
│ │ response │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │
│ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │
│ BACKGROUND SERVICES │ │
│ │ │ │
│ Health Checker Monitoring │ │
│ │ • block height polling GET / health summary │ │
│ • staleness detection GET /metrics chain stats │ │
│ │ • auto-recovery GET /api/status endpoint data │ │
│ • cooldown management GET /{secret} live dashboard │ │
│ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │
│ │
└──────────────────────────────────────────────────────────────────┘
│
▼
Upstream Endpoints (auth injected automatically)
├── Alchemy, QuickNode, Infura (Bearer / Header)
├── Self-hosted nodes (Basic Auth)
└── Public RPCs (no auth)
Features
- Multi-chain — configure any number of chains, each with its own endpoint pool
- Round-robin, weighted & latency-based rotation — distribute requests evenly, by weight, or prefer the fastest endpoint
- Method-based endpoint routing — restrict individual endpoints to specific RPC methods (e.g., route
eth_sendRawTransactionto a private mempool endpoint) - WebSocket proxy — relay WS subscriptions to upstream WSS endpoints with automatic reconnection and auth injection
- Passive health tracking — automatically detects and skips failing endpoints
- Active health checks — background block-height polling to detect stale nodes
- Configurable retries — set max retries and delay per chain, with automatic endpoint exclusion
- Hedged requests — fire parallel requests after a configurable delay to reduce tail latency
- Per-chain rate limiting — configurable request quotas per time window
- API key authentication — require clients to authenticate with
Authorization: BearerorX-Api-Key, with optional per-key rate limits - Chain ID routing — route by EVM chain ID (e.g.,
/1,/8453) in addition to path names - Response caching — per-method TTL cache with EVM/Solana presets
- Upstream authentication — per-endpoint Basic Auth, Bearer tokens, or custom headers
- Live dashboard — real-time web UI at a configurable secret path, showing chain and endpoint performance
- Metrics API — per-chain and per-endpoint stats via
/metricsand/api/status
Quick Start
Install
As a CLI
As a Library
use Turbine;
async
Or embed in your existing axum app:
let turbine = from_config.unwrap;
let router = turbine.into_router;
// Merge with your own routes, add middleware, etc.
Configuration
Full example — see config.toml for a working sample.
[]
= "127.0.0.1"
= 8080
= "my-secret" # dashboard served at /my-secret (omit to disable)
# API key auth — when any keys are defined, all proxy requests require a valid key.
# Clients pass the key via: Authorization: Bearer <key> OR X-Api-Key: <key>
[[]]
= "internal"
= "sk_internal_abc123"
[[]]
= "partner"
= "sk_partner_xyz789"
[] # optional per-key quota
= 500
= 60
# ─── Ethereum ───
[[]]
= "ethereum"
= "/ethereum"
= 1 # enables routing via POST /1
= "latency" # "round_robin" (default), "weighted", or "latency"
= [
"https://eth.llamarpc.com", # simple URL (weight defaults to 1)
{ = "https://rpc.ankr.com/eth", = 2 },
{ = "https://rpc.quicknode.com", = 3, = { = { = "x-api-key", = "key-123" } } },
# Restrict an endpoint to specific methods (e.g. private mempool)
{ = "https://private-mempool.example.com", = ["eth_sendRawTransaction"] },
]
[]
= 3 # failures before marking unhealthy (default: 3)
= 30 # seconds before retrying unhealthy endpoint (default: 30)
= 30 # how often to poll block height (default: 30)
= 10 # max blocks behind before marking stale (default: 10)
= 2 # retry up to 2 times on failure (default: 1)
= 100 # wait 100ms between retries (default: 0)
# health_method = "eth_blockNumber" # auto-detected for known chains
[]
= 100 # max requests per window
= 60 # time window in seconds
[]
= 500 # fire hedge after 500ms with no response
= 1 # max 1 additional parallel request
[]
= true
= "evm" # "evm", "solana", or omit for no preset
= 10000 # max cached entries (default: 10000)
[[]] # override TTL for specific methods
= "eth_blockNumber"
= 5
# ─── Bitcoin ───
[[]]
= "bitcoin"
= "/bitcoin"
= [
{ = "http://node1:8332", = { = { = "rpcuser", = "pass1" } } },
{ = "http://node2:8332", = { = { = "rpcuser", = "pass2" } } },
]
[]
= 3
= 30
= "getblockcount" # Bitcoin uses getblockcount instead of eth_blockNumber
# ─── Solana ───
[[]]
= "solana"
= "/solana"
= "weighted"
= [
{ = "https://api.mainnet-beta.solana.com", = 3 },
{ = "https://rpc.ankr.com/solana", = 1 },
]
[]
= 5
= 60
= "getSlot"
= 15
= 50
[]
= true
= "solana"
Configuration Reference
[server]
| Field | Type | Default | Description |
|---|---|---|---|
host |
string | required | IP address to bind to |
port |
integer | required | Port number |
dashboard_secret |
string | none | Secret path segment for the dashboard (e.g. "abc" → served at /abc). Omit to disable the dashboard entirely. |
[[server.api_keys]]
When one or more API keys are configured, all /{chain} requests must include a valid key via Authorization: Bearer <key> or X-Api-Key: <key>. Health, metrics, and dashboard routes are always open.
| Field | Type | Description |
|---|---|---|
name |
string | Human-readable label (used in logs) |
key |
string | The secret key string clients must present |
[server.api_keys.rate_limit] |
object | Optional per-key rate limit (see [chains.rate_limit] for field reference) |
[[]]
= "team-alpha"
= "sk_alpha_abc123"
[]
= 500
= 60
[[]]
= "team-beta"
= "sk_beta_xyz789"
# no rate_limit = unlimited for this key
[[chains]]
| Field | Type | Default | Description |
|---|---|---|---|
name |
string | required | Chain identifier (used in logs and dashboard) |
route |
string | required | HTTP path prefix (e.g., /ethereum) |
chain_id |
integer | none | EVM chain ID for numeric routing (e.g., 1 for Ethereum) |
rotation |
string | "round_robin" |
"round_robin", "weighted", or "latency" |
endpoints |
array | required | List of RPC endpoint URLs or objects |
Endpoint formats
# Simple string (weight = 1, no auth)
# Full object — all extra fields are optional
{ url = "https://...", weight = 3, ws_url = "wss://...", methods = ["eth_sendRawTransaction"], auth = { ... } }
| Endpoint field | Description |
|---|---|
url |
HTTP(S) RPC endpoint URL |
weight |
Relative weight for weighted/latency rotation (default: 1) |
ws_url |
Explicit WebSocket URL. If omitted, auto-derived (https:// → wss://). |
methods |
Allowlist of RPC method names this endpoint accepts. Endpoints without methods handle everything not claimed by a restricted endpoint. |
auth |
Upstream credentials (see Authentication) |
[chains.health]
| Field | Type | Default | Description |
|---|---|---|---|
max_consecutive_failures |
integer | 3 |
Failures before marking endpoint unhealthy |
cooldown_seconds |
integer | 30 |
Seconds to wait before retrying an unhealthy endpoint |
health_check_interval_seconds |
integer | 30 |
Seconds between background health checks |
max_block_lag |
integer | 10 |
Max blocks an endpoint can lag behind before being marked stale |
health_method |
string | auto-detected | JSON-RPC method for health checks |
max_retries |
integer | 1 |
Max retries on failure (total attempts = max_retries + 1) |
retry_delay_ms |
integer | 0 |
Milliseconds to wait between retries |
Auto-detected health methods: The health check method is automatically chosen based on chain name:
| Chain name contains | Health method |
|---|---|
bitcoin, btc |
getblockcount |
solana |
getSlot |
starknet |
starknet_blockNumber |
| Everything else (EVM) | eth_blockNumber |
[chains.rate_limit]
| Field | Type | Default | Description |
|---|---|---|---|
max_requests |
integer | required | Maximum requests allowed per window |
window_seconds |
integer | required | Time window in seconds |
Rate limiting is optional per-chain. When configured, requests exceeding the limit receive HTTP 429.
[chains.hedge]
| Field | Type | Default | Description |
|---|---|---|---|
delay_ms |
integer | required | Milliseconds to wait before firing a hedge request |
max_count |
integer | 1 |
Maximum number of additional parallel requests |
Hedging is optional per-chain and requires at least 2 endpoints. When the primary request doesn't respond within delay_ms, up to max_count parallel requests are fired to different endpoints at staggered intervals — whichever responds first wins. This dramatically reduces tail latency (p99) with minimal extra upstream load.
[chains.cache]
| Field | Type | Default | Description |
|---|---|---|---|
enabled |
bool | false |
Enable response caching for this chain |
preset |
string | none | "evm" or "solana" — loads default TTLs for common methods |
max_capacity |
integer | 10000 |
Maximum number of cached entries |
EVM preset caches: eth_chainId (24h), net_version (24h), eth_getBlockByNumber (5m), eth_getBlockByHash (5m), eth_getTransactionByHash (5m), eth_getTransactionReceipt (5m), eth_getCode (5m)
Solana preset caches: getGenesisHash (24h), getVersion (1h), getBlock (5m), getTransaction (5m)
Override any method's TTL with [[chains.cache.methods]]:
[[]]
= "eth_blockNumber"
= 5
Authentication
Auth is optional per-endpoint. Three methods are supported:
| Method | Config | Use Case |
|---|---|---|
| Basic Auth | { basic = { username, password } } |
Bitcoin Core, self-hosted nodes |
| Bearer Token | { bearer = "token" } |
Managed RPC providers |
| Custom Header | { header = { name, value } } |
Alchemy (x-api-key), QuickNode |
Clients don't need credentials — Turbine injects them automatically when forwarding to upstream nodes.
Dashboard
Turbine includes a built-in live dashboard served at a configurable secret path. Set dashboard_secret in [server] to enable it:
[]
= "my-secret"
The dashboard is then available at GET /my-secret. Omitting dashboard_secret disables the dashboard entirely. The secret path is the only access control — choose something unguessable.
The dashboard shows:
- Overview — total requests, global success rate, active chains, cache hit rate
- Per-chain cards — request stats, success rate bars, endpoint health visualization
- Per-endpoint details — URL, health status, latency (rolling average), block height, weight, request/success/failure counts
- Health indicators — green (healthy), yellow (degraded — some endpoints down), red (all endpoints down)
The page auto-refreshes every 5 seconds. It works on mobile too.
Monitoring Endpoints
| Endpoint | Method | Description |
|---|---|---|
/{dashboard_secret} |
GET | Live web dashboard (HTML) — only available when dashboard_secret is set |
/ |
GET | Health check — returns chain health summary as JSON |
/api/status |
GET | Detailed JSON with per-endpoint telemetry |
/metrics |
GET | Compact JSON with per-chain aggregate stats |
/api/status response shape:
/metrics response shape:
Usage
# Send a JSON-RPC request through the proxy
# Bitcoin (auth handled by Turbine, client sends plain request)
# Batch request (multiple calls in one HTTP request)
# View metrics
# Detailed status with per-endpoint data
# Route by chain ID (if chain_id = 1 configured for ethereum)
# Open dashboard in browser (replace "my-secret" with your dashboard_secret)
# WebSocket subscription (wscat or any WS client)
# With API key auth
# Or using Authorization header
Builder API Reference
All builder methods for programmatic configuration:
builder
// Server-level options
.dashboard_secret // serve dashboard at /my-secret
.api_key // require API key, unlimited
.api_key // require API key, 500 req/60s
.add_chain // start configuring a chain
.route // custom route (default: /{name})
// Endpoints
.endpoint // add endpoint (weight=1, no auth)
.weighted_endpoint // add endpoint with weight
.endpoint_with_basic_auth // Basic Auth upstream credential
.endpoint_with_bearer // Bearer token upstream credential
.endpoint_with_header // Custom header upstream credential
.endpoint_with_ws // explicit WebSocket URL override
.restricted_endpoint // method-restricted endpoint
// Rotation
.weighted // use weighted rotation
.latency_based // prefer fastest endpoint
// Health
.max_failures // failures before unhealthy
.cooldown_secs // cooldown before retry
.health_method // health check RPC method
.health_check_interval // health check interval (secs)
.max_block_lag // max block lag for staleness
.max_retries // retry up to 2 times on failure
.retry_delay_ms // 100ms between retries
// Features
.cache // enable caching
.cache_preset // load preset TTLs
.cache_max_capacity // max cache entries
.cache_method // custom method TTL (secs)
.chain_id // EVM chain ID for /{id} routing
.rate_limit // 100 requests per 60 seconds
.hedge // hedge after 500ms, max 1 extra request
.done // finish chain, return to builder
.build // build Turbine instance
How Health Tracking Works
Turbine uses two layers of health tracking:
Passive tracking happens on every request:
- If a forwarded request fails (timeout, connection error, HTTP 429/5xx), the endpoint's failure counter increments
- After
max_consecutive_failuresfailures, the endpoint is marked unhealthy and skipped - After
cooldown_seconds, the endpoint is retried — if it succeeds, it's marked healthy again - If all endpoints are unhealthy, Turbine picks the least-recently-failed one
Active health checks run in the background:
- Every
health_check_interval_seconds, Turbine calls the health method on each endpoint - It compares block heights across endpoints
- If an endpoint is more than
max_block_lagblocks behind the highest, it's marked stale (unhealthy) - When a stale endpoint catches up, it's automatically re-enabled
License
MIT