How a Transaction Flows
A Solana transaction travels from your client through the network to a block-producing leader, executes atomically on-chain, and earns votes until it reaches your chosen commitment level. Understanding this path explains latency, failures, and retry strategy.
Recipe
Quick-reference recipe card - copy-paste ready.
Client build → Sign → sendTransaction → Leader receives (Gulf Stream)
→ PoH ordering → Sealevel execution → Block propagation (Turbine)
→ Validator votes (TowerBFT) → RPC confirmation polling
import { createSolanaRpc, createSolanaRpcSubscriptions } from "@solana/kit";
const rpc = createSolanaRpc("https://api.devnet.solana.com");
const rpcSubs = createSolanaRpcSubscriptions("wss://api.devnet.solana.com");
// After sendTransaction returns a signature:
const sub = await rpcSubs.signatureNotifications(signature, { commitment: "confirmed" });When to reach for this:
- Debugging why a transaction was dropped vs rejected
- Choosing the right commitment level for your UX
- Understanding retry timing relative to blockhash expiry
- Explaining sub-second "processed" vs ~12s "finalized" to users
- Designing idempotent transaction resubmission
Working Example
import {
createSolanaRpc,
createSolanaRpcSubscriptions,
createKeyPairSignerFromBytes,
createTransactionMessage,
setTransactionMessageFeePayer,
setTransactionMessageLifetimeUsingBlockhash,
appendTransactionMessageInstruction,
signTransactionMessageWithSigners,
getSignatureFromTransaction,
pipe,
} from "@solana/kit";
import { getTransferSolInstruction } from "@solana-program/system";
import { readFileSync } from "node:fs";
const rpc = createSolanaRpc("https://api.devnet.solana.com");
const rpcSubs = createSolanaRpcSubscriptions("wss://api.devnet.solana.com");
const secret = Uint8Array.from(
JSON.parse(readFileSync(process.env.HOME + "/.config/solana/id.json", "utf8")),
);
const signer = await createKeyPairSignerFromBytes(secret);
// 1. Fetch recent blockhash (transaction lifetime)
const { value: blockhash } = await rpc.getLatestBlockhash().send();
// 2. Build and sign
const message = pipe(
createTransactionMessage({ version: 0 }),
(m) => setTransactionMessageFeePayer(signer.address, m),
(m) => setTransactionMessageLifetimeUsingBlockhash(blockhash, m),
(m) =>
appendTransactionMessageInstruction(
getTransferSolInstruction({
source: signer.address,
destination: signer.address, // self-transfer demo
amount: 1n,
}),
m,
),
);
const signed = await signTransactionMessageWithSigners(message, [signer]);
const signature = getSignatureFromTransaction(signed);
// 3. Submit
await rpc.sendTransaction(signed, { encoding: "base64" }).send();
console.log("Submitted:", signature);
// 4. Wait for confirmed commitment
const notifications = await rpcSubs.signatureNotifications(signature, {
commitment: "confirmed",
});
for await (const note of notifications) {
if (note.value.err) {
console.error("Failed on-chain:", note.value.err);
} else {
console.log("Confirmed at slot:", note.context.slot);
}
break;
}What this demonstrates:
- Blockhash binds the transaction to a recent ledger state (~60-90 seconds validity)
sendTransactionbroadcasts to the RPC, which forwards via Gulf Stream- WebSocket
signatureNotificationsis more efficient than pollinggetSignatureStatuses confirmedmeans supermajority voted on the block;finalizedis stronger
Deep Dive
How It Works
- Build - client assembles instructions, accounts, and recent blockhash
- Sign - required signers produce Ed25519 signatures
- Submit - RPC node receives and forwards to the current/upcoming leader
- Schedule - leader orders transactions using Proof of History ticks
- Execute - Sealevel runs instructions in parallel where account locks permit
- Commit - transaction changes are written to the accounts database
- Vote - validators vote on the block; TowerBFT accumulates lockouts
- Confirm - RPC reports status at the requested commitment level
Commitment Levels
| Level | Latency | Safety | Use Case |
|---|---|---|---|
processed | ~400 ms | May be dropped/reordered | UI hints only |
confirmed | ~1-2 s | Supermajority voted | Most dapp actions |
finalized | ~12-13 s | Rooted, no rollback | High-value settlements |
Failure Modes
- Dropped - never landed in a block (leader skip, low priority fee, expired blockhash)
- Rejected - landed but program returned an error (insufficient funds, constraint violation)
- Simulation failure - RPC preflight caught the error before submission
Gotchas
- Treating
processedas final - forks can reorder at this level. Fix: wait forconfirmedorfinalizedbefore irreversible UX. - Not refreshing blockhash on retry - expired transactions are silently dropped. Fix: fetch new blockhash and re-sign on each retry.
- Ignoring preflight errors -
skipPreflight: truehides simulation failures. Fix: simulate first in development; decode errors before retrying. - Polling in a tight loop - hammers RPC and still misses fast confirmations. Fix: use WebSocket signature subscriptions.
- Same signature resubmission - valid and idempotent, but useless if blockhash expired. Fix: only resubmit the same signature within the blockhash window.
- Leader skip during congestion - transactions queue longer. Fix: add priority fees via Compute Budget Program.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| WebSocket signature subscribe | Real-time confirmation UX | One-off CLI scripts |
getSignatureStatuses polling | Simple scripts | High-throughput apps |
skipPreflight: true | Known-good txs, latency critical | Development and debugging |
| Durable nonce | Long-lived signing sessions | Normal user transactions |
FAQs
What happens immediately after sendTransaction?
The RPC forwards your transaction to leaders via Gulf Stream. It is not yet confirmed - only submitted.
How long is a blockhash valid?
Approximately 60-90 seconds (about 150 slots). After expiry, the transaction is rejected even if re-submitted.
What is the difference between dropped and rejected?
Dropped: never executed in a block. Rejected: executed but the program returned an error - it is on-chain with a failed status.
Why does finalized take ~12 seconds?
TowerBFT requires sufficient validator votes across multiple slots to root a block. This takes roughly 32 slots.
Can I speed up confirmation?
Priority fees increase scheduling priority. Jito bundles help during MEV-heavy periods. You cannot skip the voting process.
What is preflight simulation?
The RPC executes your transaction against current state before broadcasting. Catches most errors early.
Is sendTransaction synchronous?
It returns a signature immediately. Confirmation is asynchronous - poll or subscribe separately.
What is Gulf Stream?
A protocol that forwards transactions directly to upcoming block leaders instead of using a traditional mempool.
Do all instructions in a transaction execute atomically?
Yes. If any instruction fails, the entire transaction is rolled back (unless a program catches and handles CPI errors).
Can a transaction partially succeed?
No. All instructions succeed or none of the state changes commit.
What commitment should a payment UI use?
confirmed for most payments. finalized for high-value or irreversible actions.
How do I trace a transaction in an explorer?
Use https://explorer.solana.com/tx/<SIGNATURE>?cluster=devnet and inspect logs, CU, and instruction breakdown.
Related
- Solana Basics - trace end-to-end example
- Slots, Blocks & Epochs - timing context
- Keypairs & Addresses - signing step
Stack versions: This page was written for Agave 4.1.1, Solana CLI 3.0.10, Anchor 0.32.1, anchor-lang 0.32.1, Rust 1.91.1, @solana/kit 7.0.0, Surfpool 0.12.0, and LiteSVM 0.6.x.