RPC node pooler with automatic discovery, health tracking, and smart routing.
A single URL that transparently routes every request to the fastest healthy node from a dynamically maintained pool. Built for Gonka.ai blockchain but compatible with majority of Cosmos SDK chains, designed to work with (almost) any Tendermint/CometBFT-based network.
Your App ---> RPC Pooler (single URL) ---> [Node A, Node B, Node C, ... Node N]
Used in the gonka.gg infrastructure.
Public RPC nodes may be unreliable: they go down, fall behind sync, rate-limit you, or respond slowly. Every project builds its own failover logic. RPC Pooler solves this once by:
- Auto-discovering 100s of nodes from chain registries, peer crawling, and participant lists
- Health checking every node continuously — tracking sync status, response times, and block heights
- Smart routing with three strategies: fastest-first (parallel), sequential failover, and broadcast
- Proxying both Tendermint RPC and Cosmos LCD/REST API through a single endpoint
- Exponential backoff and circuit breakers per node
- Emergency reset when all nodes are down
git clone https://github.com/gonkalabs/rpc-pooler.git
cd rpc-pooler
cp .env.example .env # adjust ADMIN_API_KEY if you want
# Start everything
docker compose up -d --build
# Check logs
docker compose logs -fThat's it. The pooler starts, discovers nodes, health-checks them, and routes your requests.
Replace your direct RPC node URL with the pooler:
# Before (direct node)
curl http://gonka.spv.re:26657/status
# After (through pooler)
curl http://localhost:8080/gonka-mainnet/status# JSON-RPC query
curl -X POST http://localhost:8080/ \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","method":"block","params":{"height":"100"},"id":1}'The pooler also proxies Cosmos SDK REST queries. Requests with the chain-api/ prefix are routed to LCD nodes (port 1317) instead of Tendermint RPC nodes.
# Before (direct node via explorer proxy)
curl "http://node1.gonka.ai:8000/chain-api/productscience/inference/inference/models_all"
# After (through pooler — routes to healthy LCD node on port 1317)
curl "http://localhost:8080/gonka-mainnet/chain-api/productscience/inference/inference/models_all"More examples:
# Models stats by time
# Before: http://node1.gonka.ai:8000/chain-api/productscience/inference/inference/models_stats_by_time?time_from=1764536400000&time_to=1764622799999
curl "http://localhost:8080/gonka-mainnet/chain-api/productscience/inference/inference/models_stats_by_time?time_from=1764536400000&time_to=1764622799999"
# Participants count
# Before: http://node1.gonka.ai:8000/chain-api/productscience/inference/inference/participants/count
curl "http://localhost:8080/gonka-mainnet/chain-api/productscience/inference/inference/participants/count"
# Returns: {"total":"5008"}
# Participants stats
# Before: http://node1.gonka.ai:8000/chain-api/productscience/inference/inference/participants_stats
curl "http://localhost:8080/gonka-mainnet/chain-api/productscience/inference/inference/participants_stats"| What | Direct Node URL | Pooler URL |
|---|---|---|
| Tendermint RPC | http://gonka.spv.re:26657/status |
http://localhost:8080/gonka-mainnet/status |
| LCD/REST API | http://node1.gonka.ai:8000/chain-api/productscience/... |
http://localhost:8080/gonka-mainnet/chain-api/productscience/... |
| JSON-RPC POST | http://gonka.spv.re:26657/ |
http://localhost:8080/ |
# Single-chain mode (default: first chain in config)
RPC_URL=http://localhost:8080
# Multi-chain mode
GONKA_RPC=http://localhost:8080/gonka-mainnetEverything runs inside Docker. No local Python or pip required.
# Start / stop
docker compose up -d --build # Start RPC Pooler + Redis
docker compose down # Stop everything
docker compose down && docker compose up -d --build # Rebuild and restart
# Logs and status
docker compose logs -f rpc-pooler # Tail pooler logs
docker compose ps # Show running containers
# Testing and linting (runs in a dedicated test container)
docker compose -f docker-compose.test.yml run --rm --build test pytest -v --tb=short
docker compose -f docker-compose.test.yml run --rm --build test ruff check src/ tests/
docker compose -f docker-compose.test.yml run --rm --build test mypy src/
# Utilities
docker compose exec rpc-pooler /bin/bash # Shell into the pooler container
docker compose exec redis redis-cli # Open Redis CLI
docker compose down -v --rmi local # Full cleanup (containers, volumes, images)[ Config and more <– OPEN THIS ]
server:
host: "0.0.0.0"
port: 8080
redis:
url: "redis://localhost:6379/0"
rate_limit:
enabled: true
default_rps: 100
max_concurrent_global: 500
logging:
level: "info"chain_id: "gonka-mainnet"
display_name: "Gonka Network"
seed_nodes:
- url: "http://gonka.spv.re:26657"
type: official
discovery:
peer_crawl:
enabled: true
max_depth: 2
participant_extraction:
enabled: true
base_url: "http://gonka.spv.re:8000"
endpoint: "/v1/epochs/current/participants"
list_path: "active_participants.participants"
url_field: "inference_url"
moniker_field: "index"
routing:
default_strategy: "fastest_first"
parallel_count: 3
timeout_ms: 6000
# LCD/REST API pool — proxies Cosmos SDK REST queries (port 1317)
lcd:
enabled: true
port: 1317
path_prefixes:
- "chain-api"Add any Cosmos chain by creating a new YAML file in config/chains/.
| Variable | Default | Description |
|---|---|---|
ADMIN_API_KEY |
changeme |
API key for admin endpoints |
REDIS_URL |
redis://redis:6379/0 |
Redis URL (set automatically in Docker) |
| Strategy | When Used | How It Works |
|---|---|---|
| fastest_first | Light/heavy reads (/status, /block, /tx_search) |
Fires to top N nodes in parallel, returns first success, cancels rest |
| sequential | Consensus queries, broadcast_tx_commit |
Tries nodes one-by-one until success |
| broadcast | broadcast_tx_sync, broadcast_tx_async |
Sends to ALL healthy nodes for max mempool propagation |
LCD/REST API requests always use fastest-first (3 nodes in parallel) with sequential fallback.
Override per-request with the X-RPC-Pool-Strategy header:
curl -H "X-RPC-Pool-Strategy: sequential" http://localhost:8080/gonka-mainnet/statusPin consecutive requests to the same node (useful for polling pending txs):
curl -H "X-RPC-Pool-Sticky: my-session-id" http://localhost:8080/gonka-mainnet/tx_search?query="tx.hash='ABC'"RPC Pooler finds nodes from multiple sources:
| Source | Description |
|---|---|
| Static | Seed/official nodes from config (always active) |
| Chain Registry | Fetches from cosmos/chain-registry on GitHub |
| Peer Crawl | Calls /net_info on known nodes, recursively discovers peers |
| Participant Extraction | Queries chain-specific endpoints for validator/worker URLs |
| DNS | SRV record lookup (optional) |
The participant extraction is the key differentiator — for Gonka it discovers 170+ nodes from the participant list, most of which aren't listed in any public registry.
All admin endpoints require the X-Api-Key header (or ?api_key= query param) when ADMIN_API_KEY is set.
# Pool overview (includes LCD pool status)
curl -H "X-Api-Key: changeme" http://localhost:8080/admin/pool-status
# All nodes with scores
curl -H "X-Api-Key: changeme" "http://localhost:8080/admin/nodes?sort_by=score"
# Trigger immediate discovery
curl -X POST -H "X-Api-Key: changeme" http://localhost:8080/admin/discover
# Trigger health check
curl -X POST -H "X-Api-Key: changeme" http://localhost:8080/admin/health-check
# Add a node manually
curl -X POST -H "X-Api-Key: changeme" \
"http://localhost:8080/admin/nodes?chain_id=gonka-mainnet" \
-H "Content-Type: application/json" \
-d '{"url": "http://1.2.3.4:26657"}'Prometheus metrics at /metrics:
rpc_pool_requests_total{chain, method, strategy, status}
rpc_pool_request_duration_seconds{chain, method, node}
rpc_pool_node_score{chain, node, type}
rpc_pool_nodes_total{chain, type, status}
rpc_pool_discovery_nodes_found{chain, source}
rpc_pool_failovers_total{chain}
rpc_pool_circuit_breaker_state{chain, node}
Every node gets a composite score (higher = better):
| Component | Points | Condition |
|---|---|---|
| Success rate | 0-100 | success_count / total_requests * 100 |
| Fast response | +20 | < 500ms |
| Medium response | +10 | < 1000ms |
| Official node | +10 | Node type = official |
| Validator | +7 | Node type = validator |
| In backoff | -50 | Exponential backoff active |
| Stale | -30 | Block height > 5 behind best |
| Catching up | -100 | Tendermint catching_up = true |
| Circuit open | -80 | Circuit breaker tripped |
MIT