A high-performance reverse-proxy for Solana JSON-RPC and WebSocket endpoints with Redis-backed API key authentication, per-key rate limiting, weighted load balancing, method-based routing, and automatic health checks.
- API Key Authentication: query parameter
?api-key=validated against Redis with local caching (moka, 60 s TTL). - Rate Limiting: per-key RPS limits enforced atomically in Redis (INCR + EXPIRE Lua script).
- Weighted Load Balancing: distribute requests across backends by configurable weight; unhealthy backends are automatically excluded.
- Method-Based Routing: pin specific RPC methods (e.g.
getSlot) to designated backends. - WebSocket Proxying: upgrade on the main HTTP port or a dedicated WS port (HTTP port + 1), with the same auth, rate limiting, and weighted backend selection.
- Health Checks: background loop calls a configurable RPC method per backend; consecutive-failure / consecutive-success thresholds control status transitions.
- Prometheus Metrics:
GET /metricsexposes request counts, latencies, and backend health gauges. - Admin CLI (
rpc-admin): create, list, inspect, and revoke API keys in Redis.
- Rust 2021 edition (stable)
- Redis (for API key storage and rate limiting)
# Build
cargo build --release
# Run the router (requires Redis running)
./target/release/sol-rpc-router --config config.toml
# Create an API key
./target/release/rpc-admin create my-client --rate-limit 50The router reads a TOML file (default config.toml).
port = 28899 # HTTP; WebSocket listens on 28900
redis_url = "redis://127.0.0.1:6379/0"
[[backends]]
label = "mainnet-primary"
url = "https://api.mainnet-beta.solana.com"
weight = 10
ws_url = "wss://api.mainnet-beta.solana.com" # optional
[[backends]]
label = "backup-rpc"
url = "https://solana-api.com"
weight = 5
[proxy]
timeout_secs = 30 # upstream request timeout
[health_check]
interval_secs = 30 # check frequency
timeout_secs = 5 # per-check timeout
method = "getSlot" # RPC method used for probes
consecutive_failures_threshold = 3 # failures before marking unhealthy
consecutive_successes_threshold = 2 # successes before marking healthy
[method_routes] # optional per-method overrides
getSlot = "mainnet-primary"load_config() enforces:
redis_urlmust be non-empty.- At least one backend required; labels must be unique and non-empty.
- Backend weights must be > 0.
proxy.timeout_secsmust be > 0.method_routesvalues must reference existing backend labels.
The proxy supports Solana WebSocket subscriptions (e.g. accountSubscribe, logsSubscribe) with the same authentication and load-balancing guarantees as HTTP.
sequenceDiagram
participant Client
participant Router
participant Redis
participant Backend
Client->>Router: GET / (Upgrade: websocket) + API Key
Router->>Redis: Validate Key
Redis-->>Router: OK (Owner info)
Router->>Router: Select Healthy Backend
Router->>Backend: Connect (WS Handshake)
Backend-->>Router: 101 Switching Protocols
Router-->>Client: 101 Switching Protocols
Note over Client,Backend: Bi-directional Message Pipe
Client->>Router: Message
Router->>Backend: Forward
Backend->>Router: Message
Router->>Client: Forward
- Upgrade — Clients open a WebSocket to the main HTTP port (
GET /withUpgrade: websocket) or the dedicated WS port (HTTP port + 1). Both accept?api-key=as a query parameter. - Authentication — The API key is validated against Redis (same flow as HTTP: lookup, cache check, rate-limit enforcement). Failures return
401 Unauthorizedor429 Too Many Requestsbefore the upgrade completes. - Backend Selection —
select_ws_backend()picks a healthy backend that has aws_urlconfigured, using the same weighted-random algorithm as HTTP requests. - Bi-directional Piping — After the upgrade, the proxy opens a second WebSocket to the chosen backend (via
tokio-tungstenite). Two concurrent tasks forward frames in each direction (client ↔ backend). Text, Binary, Ping, and Pong frames are relayed transparently. When either side sends a Close frame or errors out,tokio::select!shuts down the other direction. - Cleanup — On disconnect the active-connection gauge is decremented and the total session duration is recorded.
| Metric | Type | Labels | Description |
|---|---|---|---|
ws_connections_total |
Counter | backend, owner, status |
Connection attempts (connected, auth_failed, rate_limited, no_backend, backend_connect_failed, error) |
ws_active_connections |
Gauge | backend, owner |
Currently open WebSocket sessions |
ws_messages_total |
Counter | backend, owner, direction |
Frames relayed (client_to_backend / backend_to_client) |
ws_connection_duration_seconds |
Histogram | backend, owner |
Session duration from upgrade to close |
Backends that should accept WebSocket traffic must include a ws_url field. Backends without ws_url are excluded from WebSocket routing but still serve HTTP requests.
[[backends]]
label = "mainnet-primary"
url = "https://api.mainnet-beta.solana.com"
ws_url = "wss://api.mainnet-beta.solana.com" # enables WS for this backend
weight = 10# Create an API key (auto-generated)
rpc-admin create <owner> --rate-limit 10
# Create with a specific key value
rpc-admin create <owner> --rate-limit 10 --key my-custom-key
# List all keys
rpc-admin list
# Inspect a key
rpc-admin inspect <api_key>
# Revoke a key
rpc-admin revoke <api_key>
# Update a key
rpc-admin update <api_key> --rate-limit 100 --active trueRedis URL can be set via --redis-url flag or REDIS_URL env var (default redis://127.0.0.1:6379).
| Endpoint | Method | Description |
|---|---|---|
/ |
POST | Proxy JSON-RPC requests (requires ?api-key=) |
/ |
GET (Upgrade) | WebSocket proxy on main port (requires ?api-key=) |
/*path |
POST | Proxy with subpath |
/health |
GET | Backend health status (JSON) |
/metrics |
GET | Prometheus metrics |
ws://host:port+1/ |
WS | Dedicated WebSocket port (requires ?api-key=) |
cargo test # run all 35 tests
cargo test -- --list # list test namesAll tests use mocks only -- no Redis or real HTTP backends required (except localhost mock servers started in-process).