FL-02 Evidence table When it misbehaves

Reading the Failure Evidence a Transaction Leaves

A failed Solana transaction carries its own post-mortem. The ledger keeps the program logs, the error the runtime returned, the compute units consumed and the balance of every account before and after. This page reads that record field by field, and then deals with the harder case where there is no record at all.

Applies to
Any Solana transaction that did not produce the result you expected
Preconditions
A signature, or local logs from the sender if no signature exists
Pass condition
The failure is attributed to a named program, a named error and a stated cause
Out of scope
Anything about whether the trade was a good idea

To analyse a failed Solana transaction, fetch its record by signature and read four fields in order: the error, the program logs, the compute units consumed, and the balances before and after. Those four attribute almost every failure to a named program and a named cause. If there is no signature at all, the investigation moves to the sender's own logs and becomes a different problem.

What the ledger keeps

A transaction that was included in a block leaves a durable record whether it succeeded or failed. That record contains the instructions, the accounts touched, the fee charged, the log lines the programs emitted, the compute consumed, and the pre and post balances of every account in the transaction. It is a complete account of what the runtime did.

This is a stronger position than most software gives you. In a typical distributed system, the evidence for a failure lives in logs that may have rotated, on a machine that may have been replaced, in a format nobody standardised. Here the authoritative record is public, immutable and queryable by anyone with the signature.

The corollary is that a tool which does not surface signatures is discarding evidence that already exists. That is worth stating as a criterion when evaluating any automation: every attempt produces a signature, and the tool records it whether or not the attempt succeeded.

Anatomy of a transaction record

Fetch the record with getTransaction, passing the signature and a maximum supported transaction version so that versioned transactions are returned rather than rejected. The fields that matter for failure analysis are in the metadata section.

Fields of a Solana transaction record and what each one establishes during a failure investigation.
FieldWhat it tells youRead it when
errWhether it failed, and at which instruction indexAlways, first
logMessagesWhich programs ran, in what order, and what they saidAlways, second
computeUnitsConsumedTotal compute used against the limit requestedWhen the error suggests a budget problem
feeWhat the fee payer was actually chargedWhen reconciling cost against prediction
preBalances / postBalancesLamport balance of every account before and afterWhen establishing what actually moved
preTokenBalances / postTokenBalancesToken amounts by account and mintFor any swap or transfer investigation
innerInstructionsCross-program invocations the top-level instructions madeWhen the failing program was called by another
slot and blockTimeWhere in the chain it landed and roughly whenWhen correlating with other systems or with congestion

Reading the program logs

Program logs are a bracketed trace. Each program invocation opens with an invoke line carrying its call depth, emits whatever the program chose to log, and closes with either success or a failure line. Reading them is a matter of following the nesting, and the failing program is the innermost one that did not close successfully.

Program ComputeBudget111111111111111111111111111111 invoke [1]
Program ComputeBudget111111111111111111111111111111 success
Program <swap program> invoke [1]
  Program log: Instruction: Swap
  Program <token program> invoke [2]
  Program <token program> success
  Program log: AnchorError occurred. Error Code: SlippageExceeded.
               Error Number: 6001.
  Program <swap program> consumed 41,204 of 200,000 compute units
  Program <swap program> failed: custom program error: 0x1771

Three things are readable from that trace without any other information. The compute budget instruction ran and succeeded, so the limit was set as intended. The token program was invoked and returned successfully, so the failure came later. And the swap program returned its own error number rather than running out of resources, so this is a logic rejection rather than a budget or funding problem.

The consumption line is worth reading even on failures. Here 41,204 units were used against a 200,000 unit limit, which tells you the limit was generous and that raising it would not change anything. That single observation prevents the most common wrong fix in this category.

The error enum and custom codes

Errors come in two shapes. Transaction-level errors mean the transaction was rejected before or during processing for a reason unrelated to program logic: a blockhash that is not recognised, an account already in use, a signature that does not verify. Instruction errors mean a program ran and returned a failure, and they carry the index of the instruction that failed.

Within instruction errors, the interesting variant is the custom code. The runtime does not know what a program's error 6001 means; only the program's own definitions do. Frameworks widely used on Solana begin custom numbering at 6000, which is why so many real-world errors appear as small hexadecimal values just above 0x1770. Converting the hex to decimal and looking it up in that program's error list is the whole procedure.

Errors from your own dependencies are worth cataloguing once. Build a short reference for the programs your automation touches, mapping code to meaning to first action, and keep it beside the runbook. The alternative is rediscovering the meaning of the same three codes every time they appear, usually at an inconvenient moment.

The evidence table

Common failure signatures in Solana trading automation, what the evidence looks like, the usual cause and the first thing to check.
Evidence you seeUsual meaningFirst check
Transaction-level blockhash error, no execution logsThe blockhash aged out before inclusionTime between building and sending; whether the retry path rebuilds or resubmits
Custom program error just above 0x1770, after a swap instructionProgram-defined rejection, frequently a slippage guardThe configured tolerance against the price movement in that slot range
Insufficient funds error at instruction 0Balance did not cover amount plus fees plus rentWhether the balance check included rent for accounts that had to be created
Computational budget exceeded, consumption equal to the limitThe requested compute limit was too low for the path takenSimulated consumption for the same route, not a guessed increase
Account not foundAn account the instruction required did not existWhether creation is inside the tested path or was done manually once
Success on chain, but the tool reports a failureReporting defect in the tool, not an execution failureReconcile balance deltas; the money moved even though the tool says otherwise
No record for the signature at allNever included; dropped, expired or never broadcastSender-side logs; the ledger cannot distinguish these three
Repeated identical failures within secondsA retry loop resubmitting a doomed transactionRetry bounds and whether the error class is treated as retryable

The sixth row is the one that changes how people evaluate tools. A tool whose report disagrees with the ledger has a defect of a different kind from a tool that fails loudly, and it is only discoverable by someone who checks. That reconciliation is a five-minute exercise on a sample of ten transactions and it is worth doing once for any unfamiliar system.

Compute exhaustion

Every transaction runs inside a compute budget, and exceeding it terminates execution with the fee already paid. The evidence is distinctive: consumption equal to the requested limit, and a failure message about the computational budget rather than a program-defined code.

The wrong fix is to raise the limit until it stops failing. That works and it costs money on every subsequent transaction, because the priority fee is calculated from the limit you requested rather than the units you used. The right fix is to simulate the transaction, read the consumption, and set the limit slightly above the measured figure with a margin sized to how variable the route is.

Routes vary more than people expect. A swap that touches one pool consumes less than the same swap routed through two, and the router may choose differently depending on liquidity at that moment. If your automation lets the route vary, the compute limit has to accommodate the most expensive route it will accept, which is an argument for measuring several rather than one.

Reading balance deltas

The balance arrays are the ground truth about what moved. Lamport balances appear in pre and post arrays indexed by the account list; token balances appear separately, with the mint, the owner and the amount in both raw units and a decimal-adjusted form. Subtracting the two gives you the actual effect of the transaction.

This is how you verify a tool's own accounting. Take ten confirmed transactions from a run, compute the deltas from the ledger, and compare them against what the tool reported for those transactions. Agreement is a meaningful result. Disagreement is a defect that outranks anything about speed or success rate, because it means every other number the tool reports is unverified.

Watch the fee payer account specifically. Its delta includes the fee as well as any value it moved, which is why naive reconciliation produces small unexplained differences. Subtract the fee field before comparing, and the arithmetic closes exactly, which is a good check that you are reading the record correctly.

When there is no signature

The hardest case is a transaction that left no record. Querying getSignatureStatuses returns nothing, the explorer shows nothing, and the operator is certain something happened. The ledger cannot help, because the ledger only knows about transactions that were included.

Everything now depends on what the sender recorded. A tool that logs the signature at the moment of signing, before submission, can distinguish between a transaction that was never broadcast and one that was broadcast and dropped. A tool that only logs signatures after confirmation has made this case permanently undiagnosable, which is why the logging order is an acceptance criterion rather than an implementation detail.

The signature exists before the transaction does

A Solana signature is derived from the signed transaction itself, so it is fully determined the moment the transaction is signed, before anything is sent. Recording it at signing time costs nothing and preserves the ability to investigate every subsequent outcome. It also means resubmitting the identical transaction is safe, because it carries the same signature and can only be included once, while rebuilding it with a fresh blockhash creates a genuinely different transaction that can execute in addition to the first.

Failed, dropped, never sent

Three outcomes are routinely reported as one, and they have different causes and different fixes. A failed transaction was included in a block and returned an error; it cost a fee and left a record. A dropped transaction was broadcast but never included, usually because it expired or lost the competition for block space; it cost nothing and left no record. A never-sent transaction did not leave the process at all.

Distinguishing them is a reporting question, not a chain question. The chain shows failed transactions and is silent about the other two. So the criterion for any automation is that its logs let you tell dropped from never-sent, and the test is simple: kill the network connection mid-run and read what the tool says afterwards.

Getting this wrong distorts every metric built on top of it. A tool that counts only confirmed transactions as attempts will report an excellent success rate while dropping half its transactions, and the number will be arithmetically correct and completely misleading.

From one failure to a pattern

Individual failures are diagnostic; patterns are managerial. Once you have more than a handful, stop reading them one at a time and start counting: how many by error class, by venue, by hour, by wallet. A defect that appears in one wallet and not the others is a configuration difference, and that conclusion is unreachable from a single record.

This is also where measurement discipline starts to matter, because the denominator determines what the numbers mean. A campaign counted in confirmed transactions, one counted in attempts and one counted in turnover produce three different pictures of the same run, and it is worth understanding how volume campaigns are measured in practice before designing your own counters, since the choice of denominator is usually made once and then silently inherited by every report afterwards.

Anything running as a Solana DEX volume bot produces enough transactions that per-transaction reading stops scaling within one session. The practical answer is to fetch the records in bulk, extract the error field and the failing program into a table, and read the distribution. Ten minutes of counting usually replaces a day of anecdote.

Writing the evidence section

A defect report's evidence section should let a reader reach the same conclusion without asking you anything. Four elements: the signature, the verbatim error, the relevant log lines quoted rather than paraphrased, and the balance delta showing what did or did not move.

Quote errors exactly. Paraphrasing turns a specific custom code into "a slippage error", which loses the number, which is the only part that identifies which program rejected the transaction and why. The paraphrase feels more readable and is worth less than the string it replaced.

End the section with what the evidence does not establish. Usually that list includes whether the condition was transient, whether other wallets were affected, and whether the same input would fail again. Naming those gaps is what keeps the next reader from treating one transaction as a proven general behaviour.

Questions the desk gets asked

Why did my Solana transaction fail?

Read the error field of the transaction record. A transaction-level error such as a missing blockhash means it was rejected before execution. An instruction error names the index of the failing instruction and the error the program returned, which is usually a custom code defined by that program. The log lines above the failure show which program was executing when it happened.

Do failed transactions still cost a fee?

If the transaction was included in a block, yes: the fee payer is charged even though the instruction failed. If it was rejected at preflight or never included, there is nothing to charge because it never occupied block space. This distinction matters when reconciling costs after a run with many failures.

What does custom program error 0x1771 mean?

It is a program-specific code, not a runtime error, so its meaning depends entirely on which program returned it. In hexadecimal 0x1771 is 6001, and frameworks commonly used on Solana begin their custom error numbering at 6000, so this is typically the second error defined by that program. You resolve it by reading that program error list, not a general reference.

What does it mean when a signature is not found?

It means the transaction is not in the ledger the node searched. That can mean it was never broadcast, was dropped before inclusion, expired because its blockhash aged out, or that you are querying a node whose history does not go back far enough. Local sender logs are what separate those cases.

How long is a blockhash valid?

A recent blockhash remains usable for a limited number of blocks, after which a transaction referencing it is rejected. In practice this is a window of roughly a minute, which is why long-running sign-then-send flows fail intermittently and why the retry path has to rebuild rather than resubmit the same stale transaction.

Can you tell how much compute a failed transaction used?

Yes. The transaction record reports compute units consumed, and the log lines report consumption per program invocation against the limit that was requested. A failure where consumption equals the requested limit is a budget problem rather than a logic problem, and it is fixed by measuring the real requirement rather than by raising the limit arbitrarily.

Is an explorer enough, or do you need the raw record?

An explorer is enough for a single case and not enough for a pattern. Explorers render the same underlying record and are much faster to read, but any analysis across dozens of transactions needs the raw responses so the fields can be counted rather than eyeballed.

Filed under When it misbehaves by The QA Ground Desk. Behaviour described here comes from protocol documentation and from procedures the desk can run itself; any figure in an example is labelled as illustrative arithmetic and describes no real account. How the desk decides what to publish is set out in the method note.