Load-Balancing Solana RPC: Weighted HAProxy & Nginx for Bot Fleets
Solana RPC load balancing with HAProxy and Nginx: latency-weighted backends across Helius, Triton, QuickNode and a self-hosted node, with failover bots never see.
A single Helius endpoint will carry a small bot fine right up until the afternoon it doesn't — a regional blip, a rate-limit wall during a mint frenzy, or a node that silently falls 40 slots behind and starts serving stale account data. When you're running a fleet, that one failure mode multiplies across every worker pointed at the same URL. The fix isn't buying a bigger plan. It's putting a health-checked, latency-weighted proxy in front of Helius, Triton, QuickNode, and a self-hosted node so any single provider can die mid-request and your bots keep sending transactions as if nothing happened.
This is a load balancer problem with two Solana-specific twists: RPC nodes lie about their health in ways HTTP status codes don't capture, and read requests versus transaction sends want completely different routing. Get both right and you can run a fleet on commodity endpoints that behaves better than any single premium plan.
Why naive round-robin fails on Solana
Round-robin assumes every backend is interchangeable. On Solana they aren't, for three reasons.
First, slot lag is invisible to L4/L7 health checks. A node can return HTTP 200 on every request while its getSlot sits 50 slots behind the cluster. Your bot reads an account, gets a stale balance, builds a transaction against it, and eats a failed simulation. TCP is up, the node is "healthy," and you've lost the trade.
Second, providers rate-limit differently and lie about it differently. Helius returns clean 429s. Some endpoints just slow-drip responses to 800ms instead of rejecting, which looks like a healthy-but-loaded backend to a naive check. You want to weight away from a node whose p50 latency has crept up, not wait for it to hard-fail.
Third, sendTransaction and getAccountInfo have opposite priorities. Reads want the lowest-latency node that's caught up. Sends want to fan out to multiple nodes for landing probability, and ideally the ones with the best staked-connection quality. If you're doing serious send-side work, the staked-connection and SWQoS mechanics we walk through here matter more than raw latency, and a plain load balancer will actively fight you. So split the traffic: one proxy pool for reads, a separate path for sends.
The HAProxy config for read traffic
HAProxy is the right tool for the read pool because its health-check scripting and dynamic weighting are far ahead of Nginx's open-source build. Here's a working backend for four RPCs with an external agent-check driving the weights.
global
maxconn 20000
log stdout format raw local0
defaults
mode http
timeout connect 2s
timeout client 30s
timeout server 30s
option http-keep-alive
retries 2
option redispatch
frontend rpc_read_in
bind *:8899
default_backend rpc_read
backend rpc_read
balance leastconn
option httpchk POST /
http-check send hdr Content-Type application/json body "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"getHealth\"}"
http-check expect string "ok"
# agent-check on port 9999 returns a weight like "up 100%\n"
server helius helius-rpc.internal:443 check ssl verify none weight 100 agent-check agent-port 9999 agent-inter 2s
server triton triton-rpc.internal:443 check ssl verify none weight 100 agent-check agent-port 9999 agent-inter 2s
server quicknode qn-rpc.internal:443 check ssl verify none weight 80 agent-check agent-port 9999 agent-inter 2s
server selfhost 127.0.0.1:8900 check weight 60 agent-check agent-port 9999 agent-inter 2s
Two things are doing the work here. option httpchk with getHealth and an expect string "ok" catches the obvious "node is behind" case — Solana's getHealth returns an error when the node is more than ~128 slots behind, so a lagging backend gets pulled automatically. And agent-check lets an external sidecar rewrite each server's weight every two seconds based on real latency, so a node that's technically healthy but slow gets throttled down without being ejected.
The agent is a tiny process. It measures p50 over a rolling window and returns HAProxy's agent protocol string:
# agent_check.py — one per backend, listens on 9999
import socket, time, json, urllib.request, statistics
WINDOW, samples = 20, []
def probe(url):
body = json.dumps({"jsonrpc":"2.0","id":1,"method":"getSlot"}).encode()
t0 = time.perf_counter()
req = urllib.request.Request(url, body, {"Content-Type":"application/json"})
urllib.request.urlopen(req, timeout=1).read()
return (time.perf_counter() - t0) * 1000 # ms
def weight_from_latency(p50):
if p50 < 40: return 100
if p50 < 80: return 75
if p50 < 150: return 40
return 15 # still up, but push traffic elsewhere
# on each agent connection: probe, update window, emit "up <weight>%\n"
The key decision is the latency-to-weight curve. Don't make it linear. You want a node under 40ms to dominate and a node at 150ms to get a trickle — not a proportional share — because on Solana the marginal value of a 150ms read is close to zero when a 35ms node is sitting right there. The self-host node gets a lower base weight (60) on purpose: it's your fallback capacity, not your primary, and you don't want it saturated during normal operation.
Failover the bot never notices
Three settings make failover invisible. retries 2 plus option redispatch means if a backend drops a request mid-flight, HAProxy silently retries it against a different server before the client ever sees an error. option http-keep-alive keeps connections warm so a failover doesn't pay TLS setup cost. And the 2-second timeout connect ensures a dead node is abandoned fast rather than hanging your bot's request thread.
The gotcha: only redispatch idempotent reads. getAccountInfo retried against another node is fine. sendTransaction retried blindly can double-submit — usually harmless because of the transaction's recent blockhash and signature dedup, but not something you want HAProxy deciding for you. Which is exactly why sends get their own path.
Nginx for the send-side, or why you might not want a proxy at all
For sends, you generally don't want load-balancer retry logic. You want deliberate multi-submission. A thin Nginx upstream with proxy_next_upstream off gives you a stable single hop, and your bot handles fan-out itself:
upstream rpc_send {
server helius-rpc.internal:443 max_fails=3 fail_timeout=10s;
server triton-rpc.internal:443 backup;
}
server {
listen 8898;
location / {
proxy_pass https://rpc_send;
proxy_next_upstream off; # do NOT auto-retry sends
proxy_read_timeout 5s;
}
}
Honestly, for the highest-value sends we often skip the proxy entirely and go direct — dual-submitting through a normal sendTransaction alongside a Jito bundle, a pattern covered in this breakdown of sendTransaction vs. sendBundle, and for the most latency-sensitive cases forwarding straight to the leader's TPU over QUIC as described here. A load balancer in that path only adds a hop. Reads go through HAProxy; sends stay in the bot's control.
Operational details that bite
- Sticky the read pool per-worker, not per-request. A bot that reads slot N from node A and then N+1 from node B can see slots go backwards.
balance leastconnis fine within a worker's keep-alive connection; just don't rebalance mid-conversation. - Watch for connection-pool exhaustion on the self-host node. Solana validators cap RPC connections. If HAProxy opens 500 keep-alive conns to a node configured for 100, you'll get resets that look like flapping. Tune
maxconnper server. - Health-check cost is real. Four backends probed every 2s from ten HAProxy instances is 20 req/s of pure overhead per node. Use a cheap method (
getHealth, notgetBlock) and stagger the agents.
We build these fleets so bots stay latency-blind to which provider is actually serving them — the routing, the health logic, and the monitoring that catches slot lag before it costs a fill all sit in the layer below the strategy. If you want the observability side handled too, a trading dashboard wired to per-backend latency and landing rates turns this from a config file into something you can actually operate.
If you're standing up a bot fleet and want the RPC layer built to fail quietly instead of loudly, our infra and data engineering work is where that starts.
Need a bot like this built?
We design, build and run trading bots on Solana, Hyperliquid and Polymarket.
Start a projectMore from the blog
Public vs Paid RPC: How to Run a Fair Benchmark
A fair comparison of public and paid RPC endpoints must account for quotas, workload, freshness and support guarantees.
Read articleRPC Benchmark Percentiles Explained: p50, p95 and p99
Why averages hide the latency spikes that break trading bots, wallets and production blockchain applications.
Read articleHyperliquid REST vs WebSocket Benchmark: What to Measure
A benchmark plan for Hyperliquid market data that separates request latency from streaming freshness and recovery.
Read article