DEV Community

Cover image for Solana Transactions Explained for Backend Developers (With Real Failures)
Gopichand
Gopichand

Posted on

Solana Transactions Explained for Backend Developers (With Real Failures)

I've been building on Solana for the past few weeks as part of the

100DaysOfSolana challenge by MLH. Days 15–19 were all about transactions —

sending them, reading them, tracking them through confirmation stages, and
deliberately breaking them. Here's what I learned, explained for backend
developers who think in HTTP requests and database calls.

A Solana Transaction Is Not an API Call

When you hit a REST endpoint, you send a request and get a response.
If it fails, you retry. No cost, no permanent record, no cryptographic proof
it ever happened.

A Solana transaction is different in every one of those ways:

  • It requires a cryptographic signature from the fee payer's private key
  • It is permanently recorded on-chain whether it succeeds or fails
  • It expires after ~60–90 seconds based on a recent blockhash
  • It costs a fee even if it fails

That last point surprised me the most. Let me show you with real output.

The Anatomy of a Transaction

Every Solana transaction has these parts:
Signature 0: 2xQrSQU...
Account 0: srw- AWKYsCGB... (fee payer)
Account 1: -rw- 8nwngJPM...
Account 2: -r-x 11111111... (System Program)
Instruction 0
Program: 11111111... (System Program)
Transfer { lamports: 9999000000000 }
Recent Blockhash: FVSnvFfG...

  • Signature — cryptographic proof the fee payer authorized this tx
  • Accounts — every account the tx will read or write must be declared upfront
  • Instructions — the actual operations (transfer SOL, call a program, etc.)
  • Recent Blockhash — acts like a timestamp + nonce; tx expires if too old

The account permission flags (srw-, -rw-, -r-x) tell you exactly what
each account can do: signer, readable, writable, e*x*ecutable.

Solana's 3 Confirmation Levels

Unlike a database commit that's either done or not, Solana confirmation
happens in stages:

Level What it means Web2 analogy
Processed Validator included tx in a block POST reached the server
Confirmed 66%+ validators voted on the block 200 OK from load-balanced API
Finalized 31+ blocks built on top DB commit flushed + replicated

I built a live tracker that shows each stage in real time:
Tracking confirmation stages...
[Processed → Confirmed] ✅ reached in 0.3s
[Confirmed → Finalized] ✅ reached in 0.2s

Transaction successful! 🎉
Signature: 3hYmkD3m...

On devnet, Processed→Confirmed takes ~400ms. Confirmed→Finalized takes
~6–12 seconds. In production those numbers matter for UX decisions.

What Happens When Transactions Fail

This is where things get interesting. I deliberately triggered a failed
transaction by skipping preflight simulation (skipPreflight: true) and
attempting to send 9,999 SOL when my wallet only had ~6.14 SOL.

Here's the real on-chain output from solana confirm -v:
Status: Error processing Instruction 0: custom program error: 0x1
Fee: ◎0.000005
Account 0 balance: ◎6.13593 -> ◎6.135925
Account 1 balance: ◎0
Compute Units Consumed: 150
Log Messages:
Program 11111111111111111111111111111111 invoke
Transfer: insufficient lamports 6135925000, need 9999000000000
Program 11111111111111111111111111111111 failed: custom program error: 0x1

Three things stand out:

1. The fee was charged anyway.
Account 0 balance dropped by 0.000005 SOL — just the fee, not the transfer.
The network did work (verified signature, attempted execution), so it got paid.

2. The error is structured.
custom program error: 0x1 from the System Program always means insufficient
lamports. As you write your own programs, these error codes become your
primary debugging tool — like HTTP status codes but for on-chain logic.

3. The Log Messages are your stack trace.
Transfer: insufficient lamports 6135925000, need 9999000000000 tells you
exactly what was available vs what was needed. This is how you debug failed
instructions in production.

Two Types of Failure: Local vs On-Chain

Not all failures reach the chain:

Failure type Where it stops Fee charged?
CLI preflight (insufficient funds check) Never leaves your machine ❌ No
On-chain failure (skipPreflight) Executed by validators ✅ Yes

This is why production apps use simulateTransaction before submitting.
Simulation catches errors locally for free. Every on-chain failure is real
money, even if it's tiny amounts.

The Mental Model Shift

The biggest shift from Web2 to Solana transactions:

Web2: You send a request. The server decides what happens. You get a
response. If something breaks server-side, you don't pay for it.

Solana: You sign and broadcast a state change. Validators execute it
atomically. The result — success or failure — is permanent and public.
You pay regardless.

The blockhash expiry (~60–90s) also changes how you think about retries.
You can't just retry the same signed transaction forever — the blockhash
goes stale. You need to rebuild and re-sign with a fresh blockhash.

What I Built This Week

  • Day 15 — Inspected transaction anatomy: signatures, accounts, instructions, compute units
  • Day 16 — Sent first SOL transfer on devnet (<1s settlement)
  • Day 17 — Built a reusable Node.js CLI transfer tool with balance checks and Explorer links
  • Day 18 — Added live confirmation tracking (Processed→Confirmed→Finalized)
  • Day 19 — Deliberately broke transactions, read on-chain errors with solana confirm -v

All code is on my GitHub:
github.com/gopichandchalla16/100-days-of-solana


If you're coming from a Web2 background and starting with Solana, the
transaction model will feel strange at first. The fee-on-failure behavior,
the blockhash expiry, and the upfront account declaration all exist for
good reasons — they're what makes Solana fast, parallel, and predictable.
Once it clicks, it's actually elegant.

Day 20 of #100DaysOfSolana — building in public every day 🚀
Follow my progress: @GopichandAI

Top comments (0)