The Transaction
A Solana transaction is not a free-form script you send to a smart contract.
It is a signed, time-bounded message that lists every account it will touch, every program it will call, and every signature required - then either applies all of its instructions or none of them.
Transactions Basics shows concrete build and send steps; Instructions & Accounts, Priority Fees, and Blockhashes & Expiry each zoom in on one piece.
This page is the layer underneath: the single model that makes those pages feel like one system rather than a checklist of APIs.
Summary
- A transaction is a message (instructions, account metas, recent blockhash or durable nonce, fee payer) plus signatures over that message, executed atomically by the runtime.
- Insight: Almost every landing, fee, and "tx dropped" failure is a misunderstanding of accounts, expiry, or fee markets - not of program business logic.
- Key Concepts: instruction, AccountMeta, signer, fee payer, recent blockhash, atomicity, compute units (CU), priority fee, versioned transaction (v0), address lookup table (ALT), simulation.
- When to Use: Designing client submit paths, debugging silent drops versus on-chain errors, choosing base-only vs priority fees vs advanced landing, or teaching how Solana packages work before writing programs.
- Limitations/Trade-offs: Explicit account lists and short lifetimes buy parallel scheduling and replay safety, at the cost of more client complexity (refresh blockhashes, set CU budgets, handle local congestion).
- Related Topics: Sealevel account locks, Compute Budget program, durable nonces, Jito tips and bundles.
Foundations
A transaction has two top-level parts: the message (what should happen) and the signatures (who authorized that exact bytestring).
The runtime does not invent accounts or programs on your behalf; everything an instruction needs must already be named in the message.
An instruction is the smallest unit of work: a program_id, a list of accounts, and opaque data that program understands (often a discriminator plus Borsh or custom encoding).
A transaction is an ordered list of such instructions that share one fee payer, one lifetime, and one signature set.
Each account entry is an AccountMeta: a public key plus two booleans, is_signer and is_writable.
Those flags are not hints for documentation; they are the lock and authority surface the runtime enforces before any program bytecode runs.
is_signer means a matching signature must appear on the transaction (or a valid program-signed CPI path for PDAs).
is_writable means the account may have its data or lamports mutated and is locked for exclusive write during execution of this transaction.
Accounts that are only readable can be scheduled in parallel with other transactions that also only read them; two writers on the same account contend.
The fee payer is typically the first account marked as a required signer and must hold enough SOL (lamports) to cover fees for the whole transaction.
Atomicity means the full instruction list succeeds or the ledger state from those instructions is rolled back; you do not get "instruction 1 committed, instruction 2 failed half-applied."
Failed execution still can cost fees, because validators spent compute attempting the work - do not treat failure as free.
A useful analogy is a multi-step bank wire form: every account number is written on the form up front, every required signature is on the same paper, the form expires after a short validity window, and the bank either posts the entire package or rejects it - never leaves half the lines applied.
That is closer to Solana than an open-ended "call this contract and let it discover storage."
Mechanics & Interactions
Building and landing a transaction is a closed loop of compose, authorize, (optionally) simulate, submit, and confirm - always against a fresh lifetime.
compose message authorize decide
--------------- --------- ------
instructions[] ──► message ──► signatures[] ──► simulate? ──► send
account metas ▲ │
fee payer │ │ err / CU
recent blockhash ──────┘ ▼
(or durable nonce) adjust & rebuild
│
▼
RPC / leader
│
▼
execute atomically
(all ok or none)
Compose: Collect every instruction your flow needs (System transfer, SPL Token, your Anchor program, Compute Budget, and so on) and union every account those instructions require into the message account list with correct signer/writable flags.
Lifetime: Attach a recent blockhash from getLatestBlockhash (or equivalent in @solana/kit 7.0.0) so the message is unique and only valid while that blockhash remains recent - typically on the order of 60-90 seconds, depending on cluster timing and how "last valid block height" is interpreted by clients.
Without a changing blockhash, identical payloads could be replayed forever; with one, retries after expiry must rebuild and re-sign with a new hash.
Durable nonces are the alternative when you need a transaction that can sit unsigned or unsubmitted longer than a normal blockhash window (multisig, offline signing, scheduled release) - same message-and-signatures model, different lifetime mechanism. See Blockhashes & Expiry.
Authorize: Every account with is_signer: true must contribute a signature over the serialized message (wallet, backend key, hardware signer).
Missing a required signature fails before meaningful program work; extra signatures waste base fee without buying authority you did not declare.
Fees: Base fee scales with the number of signatures on the transaction.
Optional priority fee is set by including instructions from the Compute Budget program: typically setComputeUnitLimit (how many CUs you allow) and setComputeUnitPrice (microlamports per CU), so priority roughly tracks CU_limit × CU_price plus the base signature cost. Details live in The Compute Budget Program and Priority Fees.
Simulate: RPC simulation runs the transaction against current bank state without committing, returning errors, logs, and often compute units consumed.
Use simulation for preflight (wrong accounts, failed constraints, insufficient funds) and for sizing CU limit slightly above measured consumption rather than always requesting the maximum.
Send and confirm: Submission hands the signed bytes to an RPC (or a specialized landing path); leaders order and execute under Sealevel using the declared account locks.
Your client should treat "submitted" as distinct from "confirmed": refresh blockhash and re-sign on expiry, and prefer signature subscription or polling at a commitment level that matches your risk (confirmed for many app actions, finalized for high-value settlement).
Under congestion, local fee markets form around hot writable accounts: two swaps fighting the same pool account are not competing with the whole chain equally - they compete with each other for that account's write lock and the leader's packing choices.
Raising priority fee helps when you are starved for inclusion; it does not fix a wrong signer flag or an expired blockhash.
Advanced Considerations & Applications
Legacy vs versioned (v0) messages share the same mental model - instructions, accounts, signatures, atomicity - but differ in how many accounts you can fit inside the serialized size limit.
Legacy transactions embed every account pubkey inline in the message.
Versioned transactions (v0) can reference address lookup tables (ALTs): on-chain tables of addresses that the message indexes into, so more accounts fit without repeating full 32-byte keys for every entry. Conceptual deep dive: Versioned Transactions (v0) and Address Lookup Tables.
Use v0 + ALTs when composing large CPI graphs (DeFi routers, multi-hop swaps, complex mint pipelines); stay simple with fewer accounts when you do not need the room.
Simulation-driven CU limits beat static guesses: simulate, read consumed CU, set limit with a small safety margin, then set price from your urgency and observed competition.
Jito tips and bundles are an advanced landing path: you may pay a tip and/or submit an atomic bundle for MEV-aware inclusion, still under the same instruction and account rules, but through specialized infrastructure rather than only priority fee on the public path. Keep this conceptual until you need it - see Jito Tips & Bundles.
| Strategy | What you pay | Strength | Weakness | Best fit |
|---|---|---|---|---|
| Base fee only | Per-signature base cost | Cheapest; fine when the network is quiet | Easy to lose inclusion under load or on hot accounts | Devnet, low-contention transfers, non-urgent admin |
| Priority fee (Compute Budget) | Base + CU_price × CU used/limit | Works with normal RPC; tunable urgency | Overpaying wastes SOL; underpaying still drops | Most production dapps, NFT mints, retail swaps |
| Jito tip / bundles | Tip (+ optional priority) via specialized path | Stronger landing and multi-tx atomic bundles | Extra ops complexity; not a substitute for correct txs | Competitive MEV-sensitive flows, multi-step atomic sets |
None of these strategies change atomicity or AccountMeta rules; they only change how hard you bid for inclusion once the message is valid.
Clients on @solana/kit 7.0.0 should prefer v0 message builders, explicit fee payer and lifetime helpers, and simulation before mainnet submit paths.
Programs on Anchor 0.32.1 still execute only the accounts the client declared - framework macros generate instruction data and account lists, but the runtime model above remains the source of truth.
Common Misconceptions
- "A transaction is just a call to my program." It is a multi-instruction message with a global account list, fee payer, lifetime, and signatures; your program is one (or more) of the callees, not the container.
- "If the program logic is correct, the transaction will land." Correct programs still fail to land when blockhashes expire, CU budgets are too low, fees are uncompetitive, or accounts are mis-flagged.
- "Writable means the program can always write." Writable only grants the runtime lock; only the owner program may mutate account data, and your instruction must still pass every constraint.
- "Retries can resend the same signed bytes forever." Once the blockhash is stale, those signatures authorize an expired message; rebuild with a new blockhash and re-sign (unless using a durable nonce design).
- "Priority fee replaces simulation." Priority fee buys scheduling preference; simulation catches semantic failures and sizes CU - you need both under real load.
- "More accounts always means a bigger fee." Base fee tracks signatures, not account count; account count hits size limits and lock contention more than base fee directly.
- "Partial success is possible if the last instruction fails." Within one transaction, execution is atomic for the instruction list; design multi-step user flows as one tx when you need all-or-nothing.
FAQs
What is a Solana transaction in one sentence?
A signed message listing ordered instructions and all accounts they touch, executed atomically within a short lifetime set by a recent blockhash (or durable nonce).
What is the difference between a message and a transaction?
The message is the unsigned intent (accounts, instructions, lifetime, fee payer); the transaction is that message plus the signatures that authorize it.
What does an instruction contain?
A program id to invoke, an ordered list of AccountMetas, and a byte array of instruction data for that program to decode.
What do is_signer and is_writable actually control?
is_signer requires a matching signature (authority); is_writable allows mutation and takes a write lock used for parallel scheduling and safety.
Who pays fees and how is the fee payer chosen?
The fee payer is a required signer on the message (commonly the first) whose lamports balance is debited for base and priority fees for the whole transaction.
Why do transactions need a recent blockhash?
It makes each message unique (replay protection) and bounds how long the signed payload remains valid, forcing clients to refresh intent on the order of roughly 60-90 seconds.
What happens when a blockhash expires?
Validators reject or drop the transaction as too old; you must fetch a new blockhash, rebuild the message if needed, re-sign, and resubmit.
When should I use a durable nonce instead of a recent blockhash?
When the signing and submission process cannot complete inside a normal blockhash window - offline signing, multi-party approval, or delayed release - accepting the extra nonce account machinery.
Are failed transactions free?
No. Fees can still be charged for work the network performed; treat simulation as the cheap way to catch failures before you pay for a full submit path.
How do base fees and priority fees differ?
Base fee is tied to signatures on the transaction; priority fee is an optional CU price (via Compute Budget) that increases inclusion competitiveness under load.
Why set both compute unit limit and compute unit price?
Limit caps how much compute you allow (and feeds fee calculation); price is your bid per CU - together they define how much priority budget you put on the table without overshooting blindly.
What problem do versioned v0 transactions and ALTs solve?
They let messages reference accounts via on-chain lookup tables so large account lists fit in the transaction size limit without embedding every full pubkey inline.
What is simulation for if I already unit-test my program?
Simulation checks this exact message against current cluster state - accounts, balances, CU, and logs - which unit tests on a local VM do not fully replace for landing on devnet or mainnet-beta.
Why does my tx fail only when the market is busy?
Hot writable accounts create local contention; under load, low or zero priority fees lose packing races even when the same bytes succeed in quiet periods.
Do Jito tips replace priority fees?
Not as a mental model substitute: tips and bundles are an alternate or complementary landing path for competitive flows; your message must still be valid, signed, and correctly accounted.
Related
- Transactions Basics - build, sign, and send patterns
- Instructions & Accounts - AccountMeta, program ids, and data
- Versioned Transactions (v0) - v0 message format
- Priority Fees - local fee markets and CU price
- The Compute Budget Program - setComputeUnitLimit and setComputeUnitPrice
- Blockhashes & Expiry - lifetimes, retries, and durable nonces
Stack versions: This page was written for Agave 4.1.1, Solana CLI 3.0.10, Anchor 0.32.1, anchor-lang 0.32.1, Rust 1.91.1, @solana/kit 7.0.0, Surfpool 0.12.0, and LiteSVM 0.6.x.