Most Solana tutorials treat submission as a single step. You call sendTransaction, the transaction goes to the network, done.
That model is fine until you need to make submission faster or more reliable. Then it stops explaining anything, because "the network" is actually several distinct hops, each with its own failure mode, and only some of them are yours to control.
This is what actually happens between your process and a block, and which parts you can do something about.
The Path, Hop by Hop
Hop 1: Your process builds and signs
Nothing has touched the network yet. You fetch a blockhash, assemble instructions, sign.
The signature exists at the end of this step — computed locally from the transaction bytes and your keypair. This is why getting a signature back tells you nothing about whether the transaction landed. It was already determined before submission.
What you control: everything. This hop is pure local computation.
What goes wrong here: using a stale blockhash, omitting a priority fee, leaving the compute unit limit at its default. All three are decided here and cannot be repaired later.
Hop 2: Your process to the RPC
A network round trip. Your bytes travel to whatever endpoint you configured.
What you control: where that endpoint is, and whether you reuse the connection.
Connection reuse matters more than people expect. Opening a fresh TLS connection per transaction means a full handshake before your bytes move at all. On a reused connection that cost disappears — HTTPS on a warm connection costs essentially the same as plain HTTP. For a bot sending continuously, this is free latency you are otherwise paying repeatedly.
import { Agent } from "undici";
// Keep connections warm across sends instead of handshaking each time.
const agent = new Agent({
keepAliveTimeout: 60_000,
connections: 8,
});
What goes wrong here: a cold connection per send, or an endpoint on the wrong side of the world from your bot.
Hop 3: The RPC forwards toward a block producer
Your transaction is now out of your hands. The RPC node forwards it toward the validators scheduled to produce upcoming blocks.
What you control: nothing directly — but your choice of provider decided this hop's behaviour before you sent anything.
This is where stake-weighted quality of service applies. Validators accept forwarded transactions in proportion to the forwarding node's stake weight. A provider with little stake gets its traffic accepted at a lower rate, and that rate matters only when block space is contested.
What goes wrong here: deprioritisation under congestion. Invisible when the network is quiet.
Hop 4: Inclusion
The scheduled validator either includes your transaction in a block or does not. There is no queue holding it for later — Solana has no public mempool. If it is not included and you stop resending, it simply ceases to exist.
What you control: whether you are still resubmitting when the next block is produced.
If you already understand the path and want to change the third hop, a free BoltTx key takes one line.
Why "Submitted" and "Landed" Are Different Words
The three states are worth naming precisely, because collapsing them is the most expensive mistake in this area:
Submitted — an RPC accepted your bytes and returned a signature. Landed — the transaction is in a block and has a slot number. Succeeded — it landed and the instructions executed without error.
A transaction can be submitted and never land. It can land and still fail. Your monitoring needs three counters, not one boolean:
const { value } = await connection.getSignatureStatuses([signature], {
searchTransactionHistory: true,
});
if (!value[0]) metrics.increment("never_landed");
else if (value[0].err) metrics.increment("landed_failed");
else metrics.increment("landed_ok");
never_landed moving means a submission problem: fee, path, retry, or expiry.
landed_failed moving means a program problem: slippage, balance, account state.
These are unrelated. A change that fixes one does nothing for the other, and a single "success rate" number hides which you have.
Submitting Well
The pattern that works in production is not complicated, but it differs from the tutorial version in three specific ways.
import { Connection, VersionedTransaction } from "@solana/web3.js";
async function submit(
connection: Connection,
tx: VersionedTransaction,
lastValidBlockHeight: number,
): Promise<string | null> {
const raw = tx.serialize();
const signature = await connection.sendRawTransaction(raw, {
skipPreflight: true,
maxRetries: 0,
});
while (true) {
const height = await connection.getBlockHeight("confirmed");
if (height > lastValidBlockHeight) return null; // expired
const { value } = await connection.getSignatureStatuses([signature]);
if (value[0]) return signature; // landed, check .err for outcome
await connection.sendRawTransaction(raw, {
skipPreflight: true,
maxRetries: 0,
});
await new Promise((r) => setTimeout(r, 400));
}
}
skipPreflight: true. Preflight simulates your transaction against the current slot and costs a round trip. But you will not land in the current slot — you will land in a later one, against different state. So preflight can pass and tell you nothing, while spending latency you needed. Simulate during development instead, where it is genuinely useful.
maxRetries: 0. The RPC's own retry runs on a schedule you cannot observe. If you are also retrying, two components resend on different clocks. Pick one, and pick yours, because yours knows when the blockhash expires.
Resubmit until expiry, then stop. Resending identical bytes is safe: same signature, included at most once. But once lastValidBlockHeight passes, those bytes are permanently dead. Continuing to resend them accomplishes nothing — you need a new transaction with a fresh blockhash.
Instrumenting Each Hop
If submission is slower or less reliable than you want, measuring per hop tells you where to look. Most teams measure only the total and end up guessing.
const t0 = performance.now();
const { blockhash, lastValidBlockHeight } =
await connection.getLatestBlockhash("confirmed");
const t1 = performance.now();
const tx = buildAndSign(blockhash);
const t2 = performance.now();
const signature = await connection.sendRawTransaction(tx.serialize(), {
skipPreflight: true,
maxRetries: 0,
});
const t3 = performance.now();
log({
blockhashFetch: t1 - t0, // hop 2, inbound
buildAndSign: t2 - t1, // hop 1
submitCall: t3 - t2, // hop 2, outbound
blockhashAge: t3 - t1, // how much of the window you already spent
});
blockhashAge is the one people never track and should. It is the portion of your ~150-block window consumed before the transaction even reached an RPC. If it is large, your effective retry window is much shorter than you think.
What Submission Looks Like at the Far End
Real transactions through our delivery nodes: median confirmation 336ms — under one slot.
Two things worth taking from this.
Most transactions are in a block within two slots. If a strategy assumes submission and inclusion happen in the same slot, it will be disappointed. Budget for two.
Congestion widens the spread rather than shifting everything. Most transactions are unaffected; a minority take longer. That minority is where submission path quality shows up, and it clusters exactly when opportunities appear.
What You Can and Cannot Fix
Sorted by how much of it is yours:
| Hop | Yours? | Lever |
|---|---|---|
| Build and sign | Fully | Blockhash freshness, priority fee, compute limit |
| Process to RPC | Mostly | Connection reuse, endpoint location |
| RPC to validator | No | Choice of provider |
| Inclusion | Indirectly | Whether you are still resubmitting |
The useful thing about this table is the order of investigation. Work top to bottom: the top rows are cheap to test and explain most problems. Only when they are all clean does the third row become the answer — and it is the one that cannot be fixed in your code.
Where BoltTx Fits
We built one thing: the third row.
BoltTx takes signed transactions and gets them into blocks. Not indexing, not parsed history, not NFT metadata. Submissions route through our own delivery nodes in four regions, with stake-weighted routing and no public mempool exposure, so nothing observes a transaction before it lands.
You keep your keys. We never hold funds, never sign, and never modify transaction contents. Pricing follows the same logic — you include a tip in the transaction itself, paid on chain from your own wallet. If the transaction reverts, the tip reverts with it, because that is how Solana handles atomic transactions. You pay only on transactions that reach the chain.
Get a free API key. No monthly fee, no subscription:
// Pick the region closest to where your bot runs
const connection = new Connection("https://la.bolttx.io/?api-key=YOUR_KEY");
Then measure your own landing rate during congestion and compare. That number is the only one that matters.
FAQ
What is Solana transaction submission? The process of getting a signed transaction from your code into a block. It spans four hops: building and signing locally, the network trip to an RPC, the RPC forwarding toward a block producer, and the validator deciding whether to include it. Only the first two are fully under your control.
What happens after I call sendTransaction? Your bytes travel to your RPC, which forwards them toward the validators scheduled to produce upcoming blocks. If a validator includes the transaction, it lands. If not, nothing retries it for you — Solana has no public mempool, so an unincluded transaction simply stops existing.
Why does my transaction get a signature before it is submitted?
The signature is derived from the transaction bytes and your keypair, computed locally. It exists as soon as you sign, before anything reaches the network. That is why a returned signature is not evidence of landing — confirm with getSignatureStatuses instead.
How many slots does it take for a Solana transaction to land? Most transactions land within two slots. Plan around that rather than assuming the very next block.
Should I use sendTransaction or sendRawTransaction?
sendRawTransaction when you have already serialized and signed, which is the normal case for a bot. sendTransaction is a convenience wrapper that signs for you. For a retry loop, serialize once and resend the same bytes rather than rebuilding each attempt.
Does connection reuse actually matter for submission latency? Yes. A fresh TLS connection requires a full handshake before your transaction bytes move at all. On a reused connection that cost is gone, and HTTPS costs essentially the same as plain HTTP. For a continuously sending bot this is latency you would otherwise pay on every send.
What is SWQoS and how does it affect submission? Stake-weighted quality of service. Validators accept forwarded transactions in proportion to the forwarding node's stake weight. When block space is plentiful this is invisible; when it is contested, transactions arriving through a low-stake path get deprioritised — which is exactly when you need them not to be.
Why is skipPreflight recommended for production senders? Preflight simulates against the current slot, but you will land in a later slot against different state, so a passing simulation does not predict your outcome. It also costs a network round trip. Simulate during development where it is useful, and skip it in the hot path.
How do I measure submission latency properly? Instrument per hop rather than end to end: blockhash fetch, build and sign, and the submit call, each timed separately. Also track blockhash age at submit — the portion of your validity window already consumed. Most teams only measure the total and cannot tell which hop to fix.
Can I submit the same transaction to more than one endpoint? It is technically possible, since an identical signature can be included at most once. But you pay the base fee for every copy that lands somewhere, and it complicates your own accounting for very little gain. Fixing the four common causes usually produces a better result than sending more copies.
Does resending the same transaction risk paying twice? No. A signature is included at most once, so resending identical bytes is safe and is the correct retry strategy. What is not safe is rebuilding the transaction each attempt, because that produces a different signature each time.
Does submitting to multiple endpoints improve inclusion? An identical signature can be included at most once, so it is technically safe. But you pay a base fee for whichever copy lands, and it complicates accounting for little gain compared with fixing fee and routing.
How do I know which hop is slow? Instrument each separately: blockhash fetch, build and sign, and the submit call. Most teams measure only end-to-end and cannot tell which segment to fix.
What commitment should I use when submitting?
processed for the confirmation loop, since waiting for confirmed costs a slot or more. Use confirmed or better for anything you record as truth.
Why does my transaction reach the RPC but never a validator? The forwarding hop is invisible from your side. Under congestion, validators accept forwarded transactions in proportion to the forwarding node's stake weight, so a low-stake path is deprioritised exactly when block space is scarce.
Related Reading
- Solana Transaction Landing Explained
- Why Is My Solana Transaction Not Landing?
- Solana sendTransaction Best Practices
- When to Use skipPreflight
- HTTP Keep-Alive Tuning for Solana RPC