simulateTransaction runs your transaction against current chain state and tells you what would happen. That sounds like exactly what you want before spending money.
The catch is in "current." You will not land in the current slot — you will land in a later one, against state that other transactions have changed. Simulation answers a question adjacent to the one you are asking.
What Simulation Actually Returns
Three useful things, and understanding which is reliable matters:
const sim = await connection.simulateTransaction(tx, {
sigVerify: false,
replaceRecentBlockhash: true,
});
sim.value.err; // would it fail, against current state
sim.value.logs; // program log output
sim.value.unitsConsumed; // ★compute units actually used★
sim.value.accounts; // post-execution account state, if requested
★unitsConsumed is the most reliable output★, because compute usage is mostly determined by the code path rather than by market conditions. It is the number you should be setting your compute unit limit from.
err is the least reliable, because whether your transaction fails often depends on state that will have moved by the time you land.
The Two Options Worth Getting Right
sigVerify: false. Skips signature verification. Useful when simulating a transaction you have not signed yet — during development, when you are still assembling instructions.
replaceRecentBlockhash: true. Substitutes a fresh blockhash server-side. Without it, simulating a transaction whose blockhash has expired fails for the wrong reason and tells you nothing about your instructions.
// Development: measure compute, check instruction logic.
await connection.simulateTransaction(tx, {
sigVerify: false,
replaceRecentBlockhash: true,
});
// Verifying a fully-signed transaction is well-formed.
await connection.simulateTransaction(tx, {
sigVerify: true,
replaceRecentBlockhash: false,
});
If you already simulate correctly and the issue is transactions not landing, a free BoltTx key is one line to test the routing side against.
Measuring Compute Units
This is the highest-value use of simulation, and the one most people skip.
const sim = await connection.simulateTransaction(tx, {
sigVerify: false,
replaceRecentBlockhash: true,
});
const measured = sim.value.unitsConsumed ?? 200_000;
// Margin covers deeper CPI chains, larger account state, and
// paths this particular simulation did not reach.
const limit = Math.ceil(measured * 1.2);
★Simulate against realistic state, not an empty fixture.★ A swap through a pool with substantial tick data consumes noticeably more than the same swap through a shallow one. A devnet simulation with an empty pool gives you a number that fails in production.
Without a measured limit you are charged against a default well above real usage, which wastes budget precisely when fees are expensive.
Reading Logs
sim.value.logs is where a failing transaction explains itself, if you know what to look for.
if (sim.value.err) {
// The last few lines usually contain the actual cause.
console.log(sim.value.logs?.slice(-8).join("\n"));
}
Common patterns and what they mean:
| Log fragment | Cause |
|---|---|
exceeded CUs meter |
★compute limit too low★ |
insufficient funds |
balance, often including rent |
custom program error: 0x1771 |
★slippage exceeded (Jupiter-style)★ |
AccountNotFound |
account does not exist yet — often a missing ATA |
Cross-program invocation ... privilege escalation |
account not marked writable or signer |
★The error code is program-specific.★ 0x1771 means one thing in a swap program and something else elsewhere. Check the program's own error enum rather than searching the hex value alone.
Why Production Senders Skip Preflight
Preflight is simulation that the RPC runs automatically before forwarding. It is on by default, and for a trading path it costs more than it provides.
It costs a round trip. The RPC simulates, then forwards. That is latency added to every send.
It simulates against the wrong slot. You land one or more slots later, against state that other transactions have changed. A passing preflight does not predict your outcome.
It can fail on state that would have been fine. During congestion, a pool's state at preflight time and at inclusion time differ. Preflight rejects a transaction that would have landed.
// Production sending path.
await connection.sendRawTransaction(raw, {
skipPreflight: true, // ★simulate in development, not in the hot path★
maxRetries: 0,
});
★The division is clean:★ simulate during development to find compute usage and catch instruction bugs. Skip preflight in production and learn the outcome from getSignatureStatuses.
What Simulation Cannot Tell You
Worth stating explicitly, because teams debug the wrong layer for weeks.
Whether it will land. Simulation runs execution, not inclusion. A transaction that simulates perfectly can fail to reach a block because of fee, routing, or blockhash expiry — none of which simulation touches.
What the price will be. Simulating a swap gives the price against current reserves. By the time you land, other trades have moved them.
Whether you will win a race. Simulation has no concept of competition. Two bots simulating the same liquidation both get a successful result; only one of them lands.
★If simulation passes and the transaction still does not appear on chain, the problem is submission, not execution.★
Simulation in a Debugging Loop
The order that saves time when a transaction is failing:
① Did it land at all? → getSignatureStatuses
null → submission problem, simulation will not help
② It landed with an error → simulate to reproduce
③ Read the last log lines → find the actual cause
④ Fix, simulate again, resubmit
Step 1 is the one people skip. ★Simulating a transaction that never landed tells you about a problem you do not have.★
const { value } = await connection.getSignatureStatuses([sig], {
searchTransactionHistory: true,
});
if (!value[0]) {
// Never landed. Fee, routing, retry, or expiry — not execution.
} else if (value[0].err) {
// Landed and failed. Now simulation is the right tool.
}
What Landing Looks Like
Real transactions through our delivery nodes: median confirmation 336ms — under one slot.
★Landing in a later slot than you simulated against is normal, not an edge case.★ That gap is why a passing simulation is a check on your instructions rather than a prediction of your result.
Where BoltTx Fits
We handle submission, which is the part simulation cannot verify.
Submissions route through our own delivery nodes in four regions with stake-weighted routing and no public mempool exposure, so a transaction is not observable in transit before it lands. You sign locally — we never hold funds, never sign, and never modify transaction contents.
The tip travels inside the transaction, paid on chain from your own wallet, and reverts with the transaction if it fails, 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:
const connection = new Connection("https://la.bolttx.io/?api-key=YOUR_KEY");
FAQ
What does simulateTransaction do on Solana? It executes your transaction against current chain state without submitting it, returning whether it would fail, the program logs, and the compute units consumed. It predicts execution, not inclusion.
Is simulation accurate? For compute usage, yes — that is mostly determined by the code path. For success or failure, only against the state at simulation time. You land in a later slot, and market-dependent outcomes like slippage will have moved.
What is the difference between simulation and preflight? Preflight is simulation the RPC runs automatically before forwarding your transaction. It is the same operation, done at a point where it costs you latency and checks a slot you will not land in.
Should I disable preflight?
For production sending, usually yes. It costs a round trip and simulates against the wrong slot. Simulate during development where the result is actionable, and use getSignatureStatuses to learn real outcomes.
How do I get compute units from a simulation?
Read sim.value.unitsConsumed. Set your compute unit limit above it with margin, around 20%, to cover deeper CPI chains and larger account state than your simulation encountered.
What does replaceRecentBlockhash do? It substitutes a fresh blockhash server-side during simulation. Without it, a transaction whose blockhash has expired fails simulation for that reason alone, telling you nothing about your instructions.
When should I use sigVerify false? When simulating a transaction you have not signed, which is normal during development. Set it to true only when you specifically want to verify that a fully-signed transaction is well-formed.
Why did my transaction simulate fine but fail on chain? Because the state changed between simulating and landing. Other transactions moved a pool, consumed supply, or altered an account. This is expected on a chain where you land one or more slots after you simulate.
Why did my transaction simulate fine but never appear on chain?
That is a submission problem, not an execution problem. Simulation says nothing about fee, routing, retry behaviour, or blockhash expiry. Check getSignatureStatuses first.
How do I read Solana simulation logs?
The last several lines usually contain the actual cause. Look for compute-meter messages, insufficient funds, AccountNotFound, or a custom program error hex code, then check that program's error enum for what the code means.
What does custom program error 0x1771 mean? It is program-specific. In several swap programs it indicates slippage tolerance exceeded, but the same hex means something different in another program. Always check the specific program's error definitions.
Can I simulate a transaction before signing it?
Yes, with sigVerify: false. This is the normal development flow — assemble instructions, simulate to check logic and measure compute, then sign once you are satisfied.
Does simulation cost anything? No fees, since nothing lands on chain. It does consume an RPC call against your rate limit, and in a hot sending path it costs latency, which is the argument for skipping preflight there.
Should I simulate every transaction in production? Not in the sending path. Simulate during development and when debugging a failure that landed with an error. Simulating before every send adds a round trip that buys you a prediction about the wrong slot.
How do I test compute usage against realistic state? Simulate against mainnet accounts rather than an empty devnet fixture. A pool with substantial state consumes noticeably more compute than an empty one, and the difference is what makes a limit fail in production.
Can simulation tell me if I will win a race? No. Simulation has no concept of competition. Two bots simulating the same opportunity both get a successful result, and only one lands. Racing outcomes depend on fee, routing, and timing.