LND-02
Why Solana Transactions Fail, Sorted by Where They Stop
Most send failures announce themselves badly. The error string names a symptom, not a stage, and the same message can come from three different places. This entry sorts the common failures by the checkpoint that produced them and gives each one a correction with its price attached.
Fault card
- Symptom
- A send fails with a message that does not say which stage failed
- Mechanism
- Client, endpoint, network and runtime all report through the same channel
- Instrument
- Preflight logs, signature status error object, instruction index
- Correction
- Classify by fault class before choosing a fix
Solana send failures fall into four classes: the client never sent, the endpoint refused, nothing came back, or the runtime rejected it. Only the fourth class costs lamports. Sorting a failure into its class takes one look at where the error appeared, and that single sort determines which corrections are even applicable.
Four fault classes
The reason failures feel confusing is that four different systems report through one interface. A client exception, an endpoint rejection, a silent drop and a runtime error all arrive at the same place in application code and often get logged into the same counter. Once they are split, most of them are unambiguous.
| Class | Where it happened | How you know | Lamports charged |
|---|---|---|---|
| A. Never sent | Your process | Exception before the send call returned | None |
| B. Refused | The RPC endpoint | An error response from the send or simulate call | None |
| C. Silent | Between the endpoint and a leader | A signature that never gets a status | None |
| D. Rejected | The runtime, inside a block | A signature status carrying an error object | Base fee plus priority fee |
Class D is the only one that appears on chain, which has an awkward consequence: a run with a lot of class C failures looks clean in any block explorer. All the evidence of the problem lives in your own attempt log, and if you are not keeping one, the failure mode that costs the most time is also the one that leaves no trace.
Class A: the client never sent
These are the cheapest failures because they cost nothing but a retry, and the easiest to fix because the whole cause is inside your process. They cluster around three things: message size, signer completeness and account resolution.
Size failures come from the packet ceiling of 1,232 bytes that a serialised transaction must fit into. Every additional account reference and every additional instruction eats into it, so a route that grows one extra hop can push a message that has worked for weeks over the edge. Address lookup tables in versioned transactions exist to compress the account list, and adopting them is a structural fix rather than a tuning one.
Signer failures usually mean the fee payer was not among the signers, or a required authority was assumed rather than provided. Account resolution failures mean a derived address was computed with the wrong seeds or the wrong program, which produces an account that exists nowhere. All three are deterministic: they fail identically every time, which makes them easy to catch in a build test that never touches the network.
Class B: the endpoint refused
Preflight is a simulation the endpoint runs before it accepts a transaction for forwarding, and it is the most informative thing in the whole path. When it rejects, it returns the program logs from the simulated execution, and those logs usually name the failing program and the error it returned. Preserving that string is worth more than any dashboard.
The subtlety operators trip over is commitment. Preflight simulates against a commitment level, and if that level is stricter than the level your blockhash was fetched at, the simulating node can legitimately claim not to know your blockhash. The transaction is fine; the two calls simply disagreed about how recent recent is. Fetching the blockhash and running preflight at the same commitment removes an entire class of phantom rejections.
Rate limiting also lives here. An endpoint that returns a limit error has not evaluated your transaction at all, and counting those responses as transaction failures inflates a failure rate with something that is really a capacity problem. They belong in a separate counter, because the fix is request budgeting rather than anything to do with the transaction.
Preflight costs one extra round trip and returns the single most useful diagnostic string available. Skipping it buys latency and pays for that with silence plus on-chain fees for transactions that would have been caught. State the trade before making it, both directions are defensible.
Class C: nothing came back
A signature was returned, no error appeared anywhere, and the status stayed null until the blockhash aged out. This is the class that generates the most wasted effort, because there is nothing to read and the temptation is to invent a cause.
Structurally there are only a few possibilities. The transaction was not forwarded to a leader that produced a block during its validity window. It was forwarded but arrived when the leader had more candidates than space and nothing about it made it competitive. Or it arrived at a slot that was skipped, meaning the scheduled leader produced no block at all and everything queued for that slot moved on.
The correct response is not to guess between them but to change the variables one at a time and watch the class C count. Resending the identical signed bytes at a steady cadence tests the delivery hypothesis at no on-chain cost. Attaching a priority fee tests the scheduling hypothesis and costs real lamports on every landed attempt. Doing both at once tells you the count moved but not why.
The one thing that must not change during that test is the transaction itself. A rebuilt transaction with a fresh blockhash is a different transaction with a different signature, and it can execute in addition to the original rather than instead of it. That distinction is the whole subject of the retry entry in this section.
Class D: the runtime rejected it
Here the transaction landed. It was in a block, it was executed, and it charged its fee. The error object attached to the signature status describes what the runtime decided, and it is usually one of a small number of shapes.
| Runtime outcome | What it means | Correction | Price of the correction |
|---|---|---|---|
| Instruction error with an index | One instruction returned a program-specific failure | Read the program's error code, adjust that instruction | Usually a worse fill or a rebuild |
| Compute budget exhausted | Execution hit the requested unit ceiling | Raise the requested unit limit from a simulated figure | Higher priority fee, because fee scales with the request |
| Insufficient funds for fee | The fee payer could not cover base plus priority fee | Raise the balance floor for signers | Idle capital sitting in signer accounts |
| Insufficient funds for rent | An account would be left below the rent-exempt minimum | Fund account creation properly, or reuse existing accounts | More SOL locked in accounts |
| Already processed | This exact signature is already on chain | Nothing; treat it as a success, not a failure | None, but the log must classify it correctly |
The last row deserves emphasis because it is routinely miscounted. A duplicate submission of identical bytes is deduplicated by the runtime, and the response saying so is confirmation that the transaction landed once. Logging it as an error produces a failure rate that is partly composed of successes, which is worse than no measurement at all.
Reading a program error code
Instruction errors carry a number, and that number is only meaningful relative to the program that produced it. Programs written with the Anchor framework number their custom errors starting at 6000, so a hexadecimal code such as 0x1771 is decimal 6001, the second custom error that program declares. Without the program's error list, the number is opaque; with it, the diagnosis is immediate.
This is why the instruction index matters more than the code. The index tells you which instruction in your message failed, and therefore which program's error table to consult. A swap transaction that also creates a token account and sets a compute budget has several instructions, and knowing the failure came from index three rather than index zero eliminates most of the search.
Slippage guards are the most common source of these codes in trading transactions, and they are working as designed when they fire. A guard that rejects a fill outside your tolerance has protected you from a price you said you would not take. Counting it as an execution defect leads to the wrong correction, which is widening the guard until the protection is meaningless.
When the endpoint is the fault
A meaningful share of failures that look like transaction problems are really disagreements between nodes. Two endpoints do not have to be at the same slot, and any call whose answer depends on how recent the node's view is can produce a contradiction that has nothing to do with your message. Recognising that pattern early saves a lot of pointless transaction tuning.
The signature of the pattern is inconsistency without a code change. The same build succeeds on one endpoint and is refused on another, or succeeds in the morning and is refused in the afternoon with no deployment in between. Transactions do not behave that way; infrastructure does. The checks below separate the two in a few minutes.
- Fetch the blockhash and send the transaction through the same endpoint, at least while diagnosing. Splitting those two calls across providers introduces a view mismatch that produces refusals nobody can reproduce.
- Compare the block height each endpoint reports at the same moment. A node that is consistently behind will reject blockhashes that are perfectly valid elsewhere.
- Use the same commitment level for the blockhash fetch and for preflight. A stricter preflight commitment is a documented cause of a blockhash the simulating node claims not to know.
- Count rate-limit responses in their own bucket. They are a capacity signal, and folding them into a failure rate makes a quota problem look like a protocol problem.
- Record which endpoint served each attempt. Without that field, a provider-specific problem is invisible in aggregate and shows up only as unexplained variance.
- Repeat one failing build against a second provider before changing anything in the transaction. If it succeeds unchanged, the transaction was never the fault.
None of this argues that endpoints are usually to blame. It argues that endpoint faults are cheap to rule out and expensive to leave in the pile, because they contaminate every measurement taken while they are present. Ruling them out first is the same discipline as fixing expiry before touching fees.
A triage sequence that terminates
The point of a sequence is that it ends. Each step below eliminates a class, so five questions are enough to place any failure and no step depends on intuition.
- Did a signature exist? No signature means class A. The network was never involved and no send-path setting is relevant.
- Did the send or simulate call return an error? Yes means class B. Read the simulation logs and note whether it was a rate limit rather than a rejection.
- Did the signature ever receive a status before the last valid block height passed? No means class C, and the correction list is delivery and scheduling only.
- Does the status carry an error object? No means the transaction landed and succeeded, whatever else you think happened. Yes means class D.
- Does the error carry an instruction index? Yes means a program-specific failure at that index. No means a transaction-level rejection such as a compute or funding condition.
Running this on a sample of failures rather than on all of them is usually enough. Twenty classified failures will show the shape of the distribution, and the shape is what determines where effort goes. Chasing a class D slippage code while sixty percent of attempts are class C is a common and expensive misallocation.
What each class costs
Cost is the reason the classes are worth separating. Here is an illustrative accounting using only the documented base fee of 5,000 lamports per signature, with counts chosen for the arithmetic rather than observed on any run.
| Class | Attempts | Base fee cost | Other cost |
|---|---|---|---|
| A. Never sent | 15 | 0 | Developer time only |
| B. Refused | 40 | 0 | Endpoint request budget |
| C. Silent | 95 | 0 | Wall-clock time and missed market moments |
| D. Rejected | 50 | 250,000 lamports | Priority fees on top, charged in full |
The uncomfortable reading is that the cheapest class in lamports, class C, is often the most expensive in outcome, because a send that never lands at the moment it mattered has no second chance at that moment. Conversely class D is visible, quantifiable and frequently the least urgent, since a slippage guard firing is the system working.
This is also where the decision to run your own sender gets made honestly. If class C dominates and the corrections are all infrastructure ones, the effort is endpoint engineering rather than trading, and an automated Solana volume bot is one way to stop paying for that engineering with your own time. The classes still exist afterwards; what changes is who is responsible for reading them.
Whatever the answer, the classification survives the decision. Any tool worth using should be able to tell you, for a given run, how many attempts never left the client, how many were refused, how many were never seen again, and how many landed and failed. A report that offers only one number for all four is not describing the same system this entry describes. Public documentation of the underlying behaviour is available from the Solana transaction reference and is worth reading against whatever your tooling claims.
Questions this entry keeps getting
What does blockhash not found mean on Solana?
It means the node evaluating the transaction does not recognise the referenced blockhash. That happens when the blockhash has aged past the acceptance window, or when the node checking it is behind the node that issued it, or when preflight is simulating against a stricter commitment than the one the blockhash was fetched at. The three causes need different fixes.
Why does the same transaction succeed in simulation and fail on chain?
Simulation runs against a snapshot of state that no longer exists by the time a leader executes the transaction. Pool prices move, accounts are written by other transactions, and a swap that cleared its slippage guard in simulation may not clear it a second later. Simulation is a correctness gate for structure and accounts, not a prediction of outcome.
Is exceeded compute budget a client error or a network error?
It is a runtime rejection caused by a client decision. The transaction landed, consumed the units it was allowed, hit the ceiling and was reverted, so it paid its fee. The correction is to raise the requested unit limit based on a simulated consumption figure rather than to change anything about the send path.
What is the difference between a transaction error and an instruction error?
A transaction error applies to the whole message, such as an invalid signature or a blockhash the runtime will not accept. An instruction error carries an index and a program-specific code, meaning one particular instruction inside an otherwise valid transaction returned a failure. The index is the fastest way to find out which program complained.
Do failures caused by rate limiting cost lamports?
No. A request rejected by an endpoint never reaches a leader, so no fee is charged. It costs the attempt and whatever time the retry consumes, which is why rate-limit failures should be counted separately from on-chain failures rather than folded into a single error total.
Should preflight be turned off to reduce failures?
Turning off preflight does not reduce failures, it hides them and moves the cost on chain. Skipping simulation removes a round trip and removes the error message; transactions that would have been rejected locally now land, fail and pay a fee. It is a latency decision with a real price, not an error-reduction technique.
Filed under Landing. Arithmetic on this page is either a documented protocol constant or an illustrative example built from numbers you supply. If a figure here is wrong or has moved, send the desk a correction and the entry gets amended rather than quietly rewritten.