All articles
Solana·June 30, 2026·6 min read

Solana QUIC Connection Limits: Why Your Bot's Txns Get Dropped

Solana QUIC transaction drops silently kill spray-and-pray bots. How per-IP stream caps, TPU throttling, and stake-weighting decide which txns actually land.

excerpt: Solana QUIC transaction drops silently kill spray-and-pray bots. How per-IP stream caps, TPU throttling, and stake-weighting decide which txns land. tags: ["solana", "quic", "tpu", "trading-bots", "latency"] cover: signal content: Send 400 transactions a second into a Solana validator's TPU over a single connection and most of them never make it into the mempool — they get refused at the transport layer before the runtime ever sees a signature. No log, no error you can act on, just a silent drop. This is the number-one reason spray-and-pray bots underperform their backtests, and it has nothing to do with your priority fees.

The culprit is QUIC, the transport Solana adopted for TPU ingest around the 1.13/1.14 era to replace raw UDP. QUIC gives validators something UDP never did: the ability to see who is sending, cap how much any single source can push, and prioritize connections by stake. If your bot floods without understanding those caps, you are fighting the transport, not the market.

What actually happens at the TPU

The TPU (Transaction Processing Unit) is the ingest path a leader uses to receive transactions. Clients open a QUIC connection to the leader's TPU port and send each transaction as its own unidirectional stream. That framing matters, because QUIC limits streams, not packets.

Three limits stack on top of each other, and any one of them will drop you:

  • Total connection cap. A validator accepts a bounded number of concurrent QUIC connections — on the order of a couple thousand (the default max_connections is 2000 in the staked+unstaked split). Past that, new handshakes get dropped.
  • Per-IP connection cap. A single IP is allowed only a handful of connections at once (historically 8). Open a ninth from the same box and the oldest gets evicted. Run 20 worker processes on one machine each dialing the leader and they cannibalize each other's connections.
  • Per-connection stream rate. This is the one that kills spray bots. Each connection gets a throttled number of streams per unit time, sized by the sender's stake. An unstaked connection gets a tiny slice — roughly 128 streams per 100ms shared across the whole unstaked pool, divided down per-connection. Exceed it and the validator sends STREAMS_BLOCKED, then simply stops reading. Your transactions queue in your local send buffer and time out.

The stake-weighting is the part people miss. The ConnectionTable splits capacity: a chunk reserved for staked peers, the rest for everyone else. Your allowance is proportional to your_stake / total_stake. With zero delegated stake, you are competing for scraps in the unstaked lane against every other bot on the network. This is the same mechanism that makes stake-weighted QoS worth wiring into your submission path — SWQoS is not a nice-to-have, it is the difference between having a lane and not.

Why your backtest lied

Backtests assume submitted equals landed. On mainnet the funnel looks more like: you send 400 tps, the leader's per-connection stream limit passes maybe 30–50 of them, the rest are dropped at the transport before reaching the scheduler. Your fill rate craters and you blame slippage.

You can see it directly. Turn on client-side QUIC logging and watch for stream-open failures:

// solana-client / quinn: the tells that you're being throttled
// - handshake succeeds, then stream opens start failing
// - StreamLimitReached / STREAMS_BLOCKED frames
// - connection abruptly reset after a burst

let mut endpoint = Endpoint::client(bind_addr)?;
endpoint.set_default_client_config(quic_client_config);

// If open_uni() blocks or errors under load, you're stream-capped,
// not fee-capped. No amount of priority fee fixes this.
match connection.open_uni().await {
    Ok(mut send) => { send.write_all(&tx_bytes).await?; send.finish().await?; }
    Err(e) => tracing::warn!("stream refused (likely throttled): {e}"),
}

If open_uni() starts erroring or stalling the moment your send rate climbs, the transport is the bottleneck. Priority fees change ordering among transactions that landed; they do nothing for transactions that never landed.

How to actually get txns through

Stop spraying one connection

The naive fix — open more connections — runs straight into the per-IP cap of 8. So the real levers are:

  1. Get stake behind your connection. Even a small amount of delegated or leased stake moves you out of the unstaked scrap-fight and multiplies your stream allowance. This is why serious Solana MEV and arbitrage bots run against staked identities or lease stake rather than blasting from a raw VPS.

  2. Use SWQoS relays instead of hitting the TPU raw. Providers with stake will forward your transaction over their privileged connection. You inherit their stream allowance. Most production sniper bots submit through a stake-weighted endpoint precisely so a single VPS IP isn't the throttle point.

  3. Fan out across IPs, not processes. If you must self-submit, spread workers across distinct source IPs so each gets its own per-IP connection budget. One fat box with 30 threads is worse than five small boxes with six each.

  4. Batch into fewer, denser transactions. Fewer streams that each do more is strictly better under a stream-rate cap. Address Lookup Tables let you pack far more accounts into a single transaction, so one landed txn replaces three that were competing for stream slots.

Send to the right leaders, at the right time

You only need the TPU of the current and next few leaders, not the whole set. Pull the leader schedule, resolve TPU-QUIC addresses from the cluster nodes, and pre-warm connections to the upcoming 2–4 leaders before your slot. A cold QUIC handshake costs a full round trip you don't have during a hot slot.

Timing matters too. Submitting mid-slot when the leader is saturated means your streams land at the back of an already-throttled queue. Feeding off a low-latency shred source so you know exactly where the leader is in its slot — the kind of edge a ShredStream feed gives you — lets you time submission to the window where stream capacity is actually free.

The gotchas that bite in production

  • Connection eviction is LRU-ish, not fair. Open your 9th connection from an IP and the validator drops your oldest one, which might be the warm connection to the current leader you actually needed. Track your connection count per IP yourself.
  • finish() matters. A QUIC unidirectional stream isn't delivered until you finish it. Fire-and-forget code that drops the send handle before flush leaves half-sent streams that the leader discards.
  • Throttle state is per-connection and resets on reconnect — but reconnecting costs a handshake. Don't churn connections to reset a stream limit; you'll spend more time handshaking than sending.
  • Client version drift. QUIC caps have shifted across validator releases. A workaround tuned for one epoch's staged_streams math can silently regress after a network upgrade. Re-measure your real land rate every few weeks.

The honest summary: on Solana, submission is a solved problem only if you treat the transport as a first-class constraint. Fee markets decide ordering; QUIC decides admission. If your land rate is stuck and fees aren't moving it, your bottleneck is stake and streams, and the fix lives in your infrastructure layer, not your strategy code.

If your bots are landing a fraction of what they submit, our team builds the staked submission and low-latency infrastructure that turns spray into precision.

Need a bot like this built?

We design, build and run trading bots on Solana, Hyperliquid and Polymarket.

Start a project
#solana#quic#tpu#trading-bots#latency