Optimizing High-Frequency Solana Arbitrage: Intelligent RPC Failover Strategies

A deep dive into how top HFT teams structure their Rust code to automatically reroute sendTransaction calls when primary RPCs exceed latency thresholds.

  • November 23, 2025
  • Engineering
  • AllenHark Engineering
A quiet stack of pagesAn abstract drawing of light on the page's dark ground. A neat stack of thin, solid pages, the top one lit, with faint streams of light drifting toward it from the left.A quiet stack of pagesAn abstract drawing of light on the page's dark ground. A neat stack of thin, solid pages, the top one lit, with faint streams of light drifting toward it from the left.

In the unforgiving arena of high-frequency arbitrage, your primary RPC connection is your lifeline. But even the most robust, dedicated RPC nodes can experience micro-stutters, packet loss, or momentary rate limits. If your sendTransaction call hangs for even a fraction of a slot, the opportunity is gone, and your competitor has already claimed the profit.

Top HFT teams do not rely on a single endpoint. They implement Intelligent RPC Failover and Circuit Breaker patterns directly in their bot's networking layer. This article details how to structure this logic in Rust.

The Problem: Static Endpoints

A typical, naive setup looks like this:

Rust
let rpc_client = RpcClient::new("https://primary-rpc.com".to_string());
// If this hangs, your bot hangs.
rpc_client.send_transaction(&tx)?;

If primary-rpc.com stalls because of a localized network event, your bot is effectively offline during the most critical moments.

The Solution: Latency-Aware Routing

Instead of a single client, you must manage a Pool of RPCs with real-time health metrics.

1. The RPC Pool Architecture

You need a struct that maintains a list of endpoints, each with its own health score.

Rust
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use std::sync::Arc;

struct RpcEndpoint {
    url: String,
    // Exponential Moving Average of latency in microseconds
    latency_ema: AtomicU64, 
    // Error count for circuit breaking
    errors_last_minute: AtomicUsize,
    // Is this endpoint currently "healthy"?
    is_active: AtomicBool,
}

struct RpcManager {
    endpoints: Vec<Arc<RpcEndpoint>>,
}

2. The Failover Logic

When sending a transaction, you have two main strategies: Aggressive (Race) and Efficient (Failover).

Strategy A: "Race to Leader" (Aggressive)

This strategy is used when the profit potential is high enough to justify the extra bandwidth cost. You send the transaction to your 3 best-performing RPCs simultaneously. The first one to reach the leader wins.

Rust
async fn send_aggressive(&self, tx: &Transaction) {
    // Get the top 3 endpoints sorted by lowest latency_ema
    let top_3 = self.get_fastest_endpoints(3);
    
    let futures = top_3.iter().map(|rpc| {
        // Wrap in a timeout to prevent hanging
        timeout(Duration::from_millis(2000), rpc.send(tx))
    });
    
    // Wait for the first success, ignore others
    match select_ok(futures).await {
        Ok((signature, _)) => println!("Tx sent via fastest path: {}", signature),
        Err(_) => println!("All endpoints failed"),
    }
}

Strategy B: "Latency Threshold Reroute" (Efficient)

This is the standard operating mode. You try the primary, but with a very tight timeout on the initial handshake.

Rust
async fn send_smart(&self, tx: &Transaction) {
    let primary = self.get_best_endpoint();
    
    // If the connection isn't established in 50ms, abort.
    match timeout(Duration::from_millis(50), primary.send(tx)).await {
        Ok(Ok(sig)) => return sig,
        _ => {
            // Primary failed or was too slow. Immediately try backup.
            println!("Primary RPC slow, switching to backup...");
            let backup = self.get_backup_endpoint();
            return backup.send(tx).await;
        }
    }
}

Background Health Checks

Your bot should run a background thread that pings getLatestBlockhash on all endpoints twice a second. This serves two purposes:

  1. Keep-Alive: It keeps the HTTP/2 or QUIC connection warm.
  2. Telemetry: It updates the latency_ema for each endpoint.
Rust
// Pseudo-code for background monitor
loop {
    for endpoint in &endpoints {
        let start = Instant::now();
        let result = endpoint.client.get_latest_blockhash().await;
        let duration = start.elapsed().as_micros() as u64;
        
        if result.is_ok() {
            // Update EMA: New = Alpha * Current + (1 - Alpha) * Old
            let old_ema = endpoint.latency_ema.load(Ordering::Relaxed);
            let new_ema = (duration + old_ema) / 2; 
            endpoint.latency_ema.store(new_ema, Ordering::Relaxed);
        } else {
            endpoint.errors_last_minute.fetch_add(1, Ordering::Relaxed);
        }
    }
    sleep(Duration::from_millis(500)).await;
}

Infrastructure Matters

Software failover is a safety net, but hardware is the foundation. If all your RPCs are public nodes, failover won't save you.

Using AllenHark RPC as your primary gives you a private endpoint in Frankfurt or Amsterdam that only your allowlisted IP can reach, billed by the month rather than by the call — so a burst of HFT traffic does not run up a usage bill.

By combining robust Rust code with Premium Infrastructure, you ensure your transactions always find a way to the block, regardless of network conditions.