TPU Client & QUIC: Forwarding Transactions Straight to the Leader
A senior engineer's guide to the Solana TPU client and QUIC: forwarding signed transactions straight to the current and next slot leaders, bypassing RPC.
A transaction sent through sendTransaction on a public RPC node travels through more hops than most people realize: your HTTP call lands on the RPC, the RPC deserializes and re-signs a QUIC connection to whichever leaders it feels like reaching, and then it forwards your bytes on its own schedule. You are trusting an intermediary to hit the leader window on your behalf. When you are racing for inclusion in a specific slot, that intermediary is exactly the part you want to delete.
The TPU client removes it. Instead of handing bytes to an RPC and hoping, you compute the leader schedule yourself, open QUIC connections to the current and upcoming leaders, and push the signed transaction into their Transaction Processing Unit directly. This is the same path validators use to forward transactions to each other. You are just doing it from your own machine.
What the TPU actually is
Every Solana validator runs a TPU — the ingest pipeline that receives transactions, does signature verification, deduplication, and banking. Since the 1.14 era, the ingest port speaks QUIC rather than raw UDP. That change matters for anyone forwarding transactions, because QUIC connections are keyed to your identity keypair and rate-limited per source. A validator will accept a certain number of concurrent streams from a staked peer and far fewer from an unstaked one. If you show up as an anonymous connection with no stake behind it, you get the smallest slice of the pipe, and under load your streams are the first to be dropped.
That single fact drives most of the design decisions here. Getting your transaction to the leader is easy. Getting the leader to accept it during a congested slot is the hard part, and it is almost entirely about connection identity and quotas. We wrote up the mechanics of stake-weighted QoS and how staked connections change your acceptance odds in our guide to staked connections and SWQoS, and it's worth reading before you tune anything else.
Computing who to send to
The leader schedule is deterministic for an entire epoch. You fetch it once with getLeaderSchedule, cache it, and from then on you can map any slot to its leader's identity pubkey without another RPC round trip. From the identity you resolve the TPU QUIC socket address through the cluster's contact info (getClusterNodes), which you also cache and refresh periodically since gossip data drifts as nodes come and go.
The practical target is not just the current leader. Solana rotates leaders every 4 consecutive slots (a leader owns roughly 1.6 seconds of block production), and the standard TpuClient fans out to the current leader plus the next fanout_slots leaders — 12 by default in the Rust client. You forward the same transaction to all of them. Whichever leader is actually producing when your bytes arrive is the one that lands it; the others simply drop a transaction they aren't responsible for. Redundancy across the leader window is the whole point, because you don't know exactly which slot your packet will arrive in.
Here's the shape of it in Rust:
let tpu_client = TpuClient::new(
rpc_client.clone(),
&ws_url, // for slot updates
TpuClientConfig { fanout_slots: 12 },
connection_cache, // QUIC cache keyed to your identity
)?;
// Sign against a fresh blockhash first — this path does no retry for you.
let tx = build_and_sign(&payer, recent_blockhash, instructions);
tpu_client.try_send_transaction(&tx)?;
The connection_cache is the piece people skip and then wonder why latency is spiky. Opening a fresh QUIC connection costs a handshake round trip plus TLS setup every time. You want a warm, pooled connection to each leader you're likely to hit before the slot arrives, so that when you fire, you're writing into an already-open stream. Pre-warming the next few leaders' connections a slot or two ahead is the difference between single-digit-millisecond forwarding and a handshake tax on every send.
The gotchas nobody mentions in the docs
No retries, no confirmation, no mercy. try_send_transaction is fire-and-forget. There is no re-broadcast loop, no blockhash refresh, no "landed or not" signal. If your blockhash expires or the leader drops the packet, you get nothing back. You own the entire retry and confirmation loop yourself — typically by resending every 400–600ms against the leader window until you see the signature confirmed via a separate subscription, then stopping. Building that loop correctly, with proper deduplication so you're not spamming, is most of the actual work.
Unstaked connections get starved first. During congestion, a validator's QUIC layer enforces per-connection stream limits weighted by stake. An unstaked forwarder might get 1–2 concurrent streams; a heavily staked one gets hundreds. If your bot is competing in a hot slot from an anonymous IP, your packets are statistically the ones that get dropped. This is why serious operators route through a staked connection or a provider that lends stake weight.
Gossip data goes stale. The TPU address you resolved from getClusterNodes five minutes ago may point at a node that restarted on a new port. Refresh contact info on a timer and handle connection failures by re-resolving rather than retrying the dead socket.
QUIC is not a fallback to UDP. Modern validators reject the old UDP TPU port outright. If you copied a 2022 tutorial that opens a UDP socket to the leader, it silently sends into a void. Everything is QUIC now.
When this is worth it — and when it isn't
Direct TPU forwarding buys you the lowest-latency, most-controlled path to the leader that exists without running your own validator. For a market maker quoting tight spreads, or a liquidation bot where 200ms decides whether you or someone else gets the position, that control is the whole game. It's the same reason we build the ingest side of client infrastructure directly against the validator's data plane rather than through convenience layers.
It is not worth it for casual submission where an RPC's built-in forwarding is fine, or where you'd be better served by a bundle. Direct TPU and Jito bundles solve different problems — one is about latency to the leader, the other about atomic inclusion and MEV protection — and the sharpest setups run both, picking per-transaction. We compared those two submission paths head to head in sendTransaction versus sendBundle, and the dual-submission pattern there pairs naturally with a warm TPU client.
One more architectural note: the TPU client needs a fast, reliable feed of slot updates to know who the current leader is, and polling an RPC for that is too slow. Drive it off a streaming source instead. If you're already running a Yellowstone gRPC feed for account and slot data, tap the same multiplexed connection for slot notifications and your leader tracking stays a fraction of a slot behind tip. That same low-latency plumbing is what makes a real-time trading dashboard actually reflect what your bot is doing on-chain instead of showing you a stale RPC view.
The uncomfortable truth is that the code to open a QUIC connection and push bytes is the easy 20%. The other 80% is stake-weighting, connection warming, leader tracking off a streaming feed, and a retry loop that doesn't turn into a self-inflicted denial of service. If you'd rather have that path built and instrumented correctly the first time, that's exactly the kind of low-latency plumbing we handle in our Solana market-making infrastructure work.
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