Guide
Solana transaction simulation explained
Every Solana transaction costs a base fee even when it fails on-chain. Before you sign, wallets and well-built dApps run a simulation — a dry-run that executes your transaction against the current ledger state without broadcasting it. Simulation catches insufficient balance, wrong accounts, program logic errors, and compute exhaustion before you spend SOL. Understanding how it works saves money, speeds debugging, and explains why Phantom sometimes blocks a transaction with a cryptic preflight error.
What simulation actually does
Solana validators process transactions in a sandboxed runtime. Simulation replays that same pipeline locally on an RPC node: load accounts, run each instruction in order, update balances, and return success or failure — plus detailed logs. Nothing is written to the real chain; no signature is required for a read-only simulation (though signed transactions can be simulated too).
The RPC method is simulateTransaction. Under the hood the node:
- Deserializes your base64-encoded transaction bytes.
- Fetches the latest account data for every address referenced (signers, writable accounts, program IDs).
- Executes instructions sequentially inside the BPF runtime.
- Records
logMessages—msg!output from programs, plus system-level errors. - Returns
errif any instruction failed, plusunitsConsumedfor compute metering.
Wallets call this automatically when you click Approve. Backend services call it before
sendTransaction to avoid broadcasting doomed transactions during high traffic.
If you have ever seen "Transaction simulation failed" in Phantom, that is
preflight — simulation returned an error and the wallet refused to sign.
Preflight in wallets vs manual simulation
Wallet preflight is simulation with guardrails. Phantom, Solflare, and Backpack simulate by default, then show a human-readable summary: SOL moving, token transfers, program interactions. If simulation fails, the wallet blocks signing rather than letting you pay a fee for a transaction that will revert.
Developers can simulate manually for finer control:
- JavaScript / TypeScript —
connection.simulateTransaction(tx, { sigVerify: false })via@solana/web3.js. SetsigVerify: falsewhen the transaction is unsigned (common during building). - CLI — pipe a serialized transaction to
solana simulate-transactionor usesolana confirm --simulatepatterns in scripts. - Anchor —
anchor simulateor test suites that callprogram.methods...simulate()for local validator runs.
Manual simulation is essential when building multi-instruction transactions (swap +
stake + close account) where the wallet summary is too coarse. You inspect
simulation.value.logs line by line to see which instruction failed.
Reading simulation output
A successful simulation returns err: null and an array of log strings.
A failed simulation still returns logs — often the most useful part. Typical patterns:
Program log: Error: insufficient funds— not enough SOL in the payer or source token account.custom program error: 0x1— Anchor-style custom error; map to your IDL or program source.Instruction #2 Failed— the index tells you which instruction in a batch broke.Computational budget exceeded— transaction needs more compute units; raise the budget instruction or optimize the program.AccountNotInitialized— you referenced an ATA or PDA that does not exist yet; add a create instruction first.
Cross-check failed simulations against our transaction failed guide for user-facing fixes (wrong network, stale blockhash, insufficient SOL for fees). For on-chain receipts after a real send, use reading transactions on Solscan.
Common simulation failure causes
Stale or missing blockhash
Transactions embed a recent blockhash as a TTL. If your builder cached a blockhash for
too long, simulation may fail with Blockhash not found. Fetch a fresh
blockhash with getLatestBlockhash and rebuild. Wallets usually refresh
automatically; backend cron jobs do not.
Account state drift
Simulation uses the RPC node's view of account data right now. Between building and signing, someone else may close an account, drain a pool, or change a config. A simulation that passed five seconds ago can fail at send time — and vice versa. High- contention DeFi pools are the classic case. Retry with fresh account fetches.
Missing signers or wrong authority
Simulation with sigVerify: false skips signature checks, so you can test
unsigned txs. But program logic still enforces that the right pubkey signed. If your
transaction is missing a required signer, simulation may pass (unsigned) yet fail on
send. Always simulate the final signed bytes before mainnet broadcasts when automating.
Compute and priority fees
Heavy programs consume compute units (CU). Default budget is 200,000 CU per instruction
unless you add a SetComputeUnitLimit instruction. Simulation reports
unitsConsumed — use it to set limits without overpaying. During congestion,
low priority fees cause transactions to drop without ever landing; simulation cannot
predict queue position, only execution correctness. See
priority fees explained
for tuning tips.
RPC quality
Simulation is only as accurate as the node's ledger snapshot. Lagging or rate-limited RPC endpoints return stale account data or time out mid-simulate. If preflight fails intermittently, switch providers — our RPC endpoints guide covers public vs dedicated nodes and 429 fallbacks.
When simulation lies (edge cases)
Simulation is a best-effort preview, not a guarantee:
- Passed simulation, failed on-chain — state changed between simulate and confirm; blockhash expired; or the leader processed a conflicting transaction first.
- Failed simulation, would have succeeded — rare, but stale RPC data or simulating against the wrong cluster (devnet builder, mainnet send) causes false negatives.
- Oracle / slot-dependent logic — programs reading
Clockor external oracle accounts may behave differently at execution time under network jitter. - Disabled preflight —
sendTransactionacceptsskipPreflight: true. Power users and some bots use this to shave latency; you trade safety for speed and may pay fees on failures.
Production backends should simulate, then send with preflight enabled unless you have a measured reason not to. Never disable preflight in user-facing flows to hide bugs.
Developer workflow: simulate before send
A minimal TypeScript pattern (conceptual — adapt to your stack):
const { blockhash, lastValidBlockHeight } =
await connection.getLatestBlockhash('confirmed');
tx.recentBlockhash = blockhash;
tx.feePayer = payer.publicKey;
const sim = await connection.simulateTransaction(tx, {
sigVerify: false,
commitment: 'confirmed',
});
if (sim.value.err) {
console.error('Simulation failed:', sim.value.err);
console.error('Logs:', sim.value.logs?.join('\n'));
throw new Error('Aborting send — fix simulation first');
}
console.log('CU used:', sim.value.unitsConsumed);
// sign, then sendTransaction with skipPreflight: false
Log unitsConsumed during development to right-size compute budgets. On
mainnet, add a priority fee instruction when simulation shows high CU usage and
mempools are busy. Test the same flow on
devnet first
— simulation semantics match mainnet, but SOL is free.
Merchants verifying inbound payments simulate less often; they read confirmed transactions instead. Our payment verification guide covers the receive side. Simulation is for the send path.
Security note: simulation is not approval
A passing simulation means this transaction would execute successfully given current state — not that it is safe to sign. Malicious dApps can simulate harmless-looking transfers while the signed bytes drain your wallet via a different instruction layout. Always read the wallet's human summary, verify the dApp domain, and follow wallet security practices. Simulation catches program errors; it does not catch phishing.