Engine Room

The Transaction Pipeline: Intent to Confirmation

One swap passes through seven stages between a scheduler and a confirmed signature. Each stage has a contract it owes the next one, and each has a distinct way of failing when the network is busy rather than when your code is wrong.

The Engine Room Desk 2375 words 11 min read Updated 13 August 2026

Component sheet

Component class
Deterministic transform plus network boundary
Inputs
One intent, one lease, one route quote
Outputs
Signed bytes, a signature, an outcome record
Hardest boundary
Stage six to seven, where bytes become irreversible

A swap moves through seven stages: route selection, instruction assembly, compute budget, size fitting, blockhash and deadline, signing and persistence, then submission and confirmation. The builder owns the first six and must be deterministic across all of them. The sender owns the seventh and is the only stage that touches the network more than once.

Splitting the work this way is not bureaucracy. It is what makes a retry safe. If the builder produces a stable artefact and persists it before the sender sees it, then a retry is a resend of identical bytes and the network deduplicates for you. If the two are entangled, every retry is a new transaction, and the engine has no way to distinguish a slow confirmation from a lost one without risking a duplicate.

Seven stages at a glance

StageProducesFails asOwner
1. RouteA venue, a path, an expected outputStale quote, no route, unacceptable impactBuilder
2. AssembleAn ordered instruction listMissing account, wrong program argumentBuilder
3. BudgetCompute unit limit and unit priceOverrun, or overpaying on every attemptBuilder
4. FitA message within the packet limitSerialised size exceeds 1232 bytesBuilder
5. BlockhashA recent blockhash and a deadlineExpiry before landingBuilder
6. SignSigned bytes, a signature, a durable recordSending before persistingBuilder
7. SendAttempts, a slot, an outcomeRate limit, silence, ambiguous resultSender

Read the failure column downward and a pattern appears. Stages one to four fail loudly and locally: you get an error, nothing was signed, nothing was spent. Stages five to seven fail quietly and remotely, and those are the ones that cost money. Most engineering effort is best spent on the bottom three rows.

Stage 1: intent to route

The intent says move this much of this token on this venue. The route says exactly which pools and which hops will do it, and what output to expect. Whether you compute the route yourself against a specific pool or ask an aggregator for one is a real architectural fork with consequences at every later stage.

A direct pool route is short, predictable and small on the wire. You know the accounts in advance, the instruction list is stable, and the transaction almost certainly fits without special measures. The cost is that you are bound to one pool's liquidity and one price, and when that pool is thin you have no alternative.

An aggregated route can find better pricing across venues, and it can also return a three-hop path that touches a dozen accounts and pushes you straight into the packet size problem. It also means the route is a fetched artefact with an age, and an old quote is a specific kind of trap: the transaction builds fine, simulates fine, and lands against a price that has moved. Recording the quote timestamp alongside the intent is the minimum defence.

Trade-off: better price versus a bigger transaction

Multi-hop routing genuinely improves output on thin pairs. It also enlarges the account list, which pushes against the packet limit, raises the compute the transaction consumes, and increases the number of ways it can fail mid-execution.

For a run made of many small swaps, the simpler single-hop route is often the better engineering choice even when it prices slightly worse, because a route that occasionally fails to fit costs more in failed attempts than it saves in basis points.

Stage 2: instruction assembly

Assembly turns a route into an ordered list of instructions. In practice a swap transaction is rarely one instruction. It typically carries compute budget instructions first, possibly an instruction to create an associated token account if the executor does not have one for the output mint, the swap itself, and sometimes a close instruction to reclaim an account afterwards.

Order matters more than it appears. Compute budget instructions must be present in the transaction for the requested limit and price to apply, and an account creation must precede any instruction that uses that account. Getting the order wrong produces an error that names an account rather than an ordering problem, which is why this stage is worth unit testing against a fixed fixture rather than debugging live.

The other decision at this stage is whether to batch. Two swaps in one transaction share a signature fee and a compute budget, which is cheaper per swap, but they also share an outcome: if one fails, both do. For a run that cares about total activity rather than about any individual swap, batching is attractive; for a run where each swap is a separate accounting unit, the shared failure makes reconciliation harder. Decide it once, at design time, and record which mode a run used.

Stage 3: compute budget and fee

Solana charges a base fee per signature and, optionally, a priority fee derived from two values you set explicitly: the compute unit limit you request and the compute unit price you are willing to pay per unit. The product of those two, in micro-lamports, is what the priority portion costs. Both are instructions in the transaction, which means both are part of the signed artefact and cannot be adjusted on a resend.

The common failure here is setting one and not the other. Requesting a large limit without raising the price does not make you competitive; it just reserves more compute than you need, which can make you less likely to be included when blocks are full. Raising the price without measuring the limit means you pay the multiplier on units you never use. The pairing is the point, and the note on compute budget works through how to derive both from simulation.

The illustrative arithmetic is simple enough to state here. If a swap actually consumes 90,000 compute units and you request 200,000 at a unit price of 10,000 micro-lamports, you are paying the price across the requested limit rather than the consumed amount, and across several hundred swaps that gap becomes the largest avoidable line in the run. Measuring once and setting the limit near the observed consumption plus a margin is the whole optimisation.

Stage 4: fitting the packet

A Solana transaction must fit within a 1232-byte packet. That budget covers signatures, the account list, the instruction data and the blockhash. Accounts are the expensive part: every distinct account a transaction references occupies space in the message, and a multi-hop route through several pools can reference twenty or more.

The builder must therefore check the serialised size before signing, not discover it through a submission failure. A transaction that is too large is not a network problem you retry; it is a construction problem you must solve by shortening the route, splitting the work, or moving addresses into a lookup table. Silently exceeding the limit and letting the sender find out wastes an attempt and pollutes your error metrics with a failure that had nothing to do with the network.

When the route genuinely needs more accounts than fit, address lookup tables are the mechanism the protocol provides. They are covered in their own note, because they buy real headroom and cost you a setup transaction, some locked rent, and a coupling between your builder and a table that must exist on chain before the transaction referencing it can be built.

Stage 5: blockhash and deadline

Every transaction references a recent blockhash, and validators accept only blockhashes from a recent window of roughly 150 blocks. That window is your deadline, and it is the single most important number in the retry design, because it converts an open-ended question into a bounded one: either the transaction lands before its blockhash expires or it never will.

The deadline starts at build time. A transaction built and then held in a queue for twenty seconds has already spent a meaningful fraction of its life before the first submission. This is why the blockhash belongs to the builder and why the built artefact must carry its own expiry: any component that needs to decide whether to keep trying must be able to read the deadline off the transaction rather than recompute it.

The practical rule that follows is to build late. Fetch the blockhash and sign as close to submission as the design allows, rather than pre-building a queue of transactions that age while they wait. Pre-building looks like a throughput optimisation and is usually a way of manufacturing expiries during exactly the congestion windows where you wanted the extra throughput.

Stage 6: sign, persist, then send

This is the boundary where a plan becomes an irreversible fact, and the ordering of three operations decides whether the engine can recover from a crash. Sign, then persist the signed bytes and the signature, then hand them to the sender. Never send first and persist afterwards.

tx = build(intent, lease, route, blockhash)
sig = signature_of(tx)

ledger.record_pending(intent.id, sig, tx.bytes, tx.valid_until)   durable write
sender.submit(tx, deadline = tx.valid_until)                      network call

crash between the two lines
  -> nothing was sent, the pending record is resolvable, no value moved
crash after the second line
  -> the signature is on disk, so recovery can query the chain for it

The reason this ordering is non-negotiable is the crash case. If the process dies after submitting but before recording, there is a signature on the network that your ledger has never heard of, the fleet's believed balance is wrong, and the only way to find it is to scan the account's history and guess. Recording first costs one durable write per transaction and removes that entire failure class.

Persisting the bytes rather than just the signature matters too, because a resend needs the bytes. An engine that stores only the signature can query for an outcome but cannot re-broadcast, which means its only retry option is to build something new, and building something new is exactly what the whole ordering exists to avoid.

Stage 7: submission and confirmation

Submission is cheap and confirmation is where the ambiguity lives. A submission can be accepted, refused for a rate limit, refused for a malformed transaction, or answered with a timeout that tells you nothing. Only the third of those is unambiguous. A timeout means the request failed, not that the transaction did not reach the network.

Confirmation therefore has to be pull-based and evidence-driven. The engine holds a signature and a deadline, and it asks the chain about that signature until it gets one of three answers: it landed at a slot, its blockhash expired without landing, or the deadline passed while the answer was still unknown. The third answer is not a failure to be retried; it is a state that must be recorded and resolved before the account signs again.

Commitment level is the other decision here. Recording outcomes at a lower commitment gives faster feedback and occasionally records something that does not survive; recording at a stricter level is slower and safer. A defensible split is to show progress at the faster level and to write the ledger at the stricter one, provided the difference is visible to the operator rather than hidden. Engines that use the fast level everywhere produce run summaries that are confidently slightly wrong, which is the worst kind of wrong for a Solana trading volume bot whose entire output is a record of what happened.

Simulation as a gate, not a formality

Simulating a transaction before sending it runs it against current state without committing anything, and returns the compute consumed, the logs, and any error. Used properly it catches most of the failures in stages one to four before they cost an attempt.

Use it as a gate in three specific places. Once per configuration change, to measure compute consumption and set the unit limit from evidence. Once per new route shape, to confirm the account list and the instruction ordering are correct. And on a sampled basis during a run, to detect drift, because a route that worked an hour ago can stop working when a pool's state changes.

Simulating every transaction is possible and usually not worth it. It doubles your request count against an endpoint budget that is already the tightest constraint in most designs, and it delays the transaction, which spends part of the blockhash window you were trying to protect. Sampling gives you most of the signal at a fraction of the cost.

Why the builder must be deterministic

Given the same intent, lease, route and blockhash, the builder should produce byte-identical output. This is a stronger requirement than it sounds, and it is broken by ordinary-looking code: a timestamp in a memo, an account list assembled from a hash map with unstable iteration order, or a fresh quote fetched inside the build function.

Determinism is what makes the resend path correct, what makes a fixture-based test meaningful, and what makes a post-mortem possible. When a transaction fails in a way you did not expect, you want to rebuild it exactly from its recorded inputs and inspect it. If the rebuild differs, you are debugging a different transaction from the one that failed.

  • No wall-clock values in the message except the blockhash, which is an input rather than a lookup.
  • Account lists ordered explicitly, never by iteration over an unordered collection.
  • Quotes passed in as arguments, never fetched inside the builder.
  • Every input to the build recorded alongside the output, so the artefact can be reproduced.

Pipeline review checklist

  • Does the builder check serialised size against the packet limit before signing?
  • Do the compute unit limit and unit price come from measurement rather than from a default?
  • Is the blockhash fetched by the builder and carried with the artefact as a deadline?
  • Are signed bytes persisted before the first submission, not after?
  • Does the retry path resend stored bytes, or does it call the builder again?
  • Does the confirmation path distinguish landed, expired and unknown as three outcomes?
  • Is the commitment level used for the ledger stricter than the one used for the progress display?
  • Can any recorded transaction be rebuilt byte-for-byte from its stored inputs?

Take the last question seriously, because it subsumes several of the others. An engine that can rebuild any past transaction from its inputs has, by construction, a deterministic builder, a complete input record and a persistence order that puts the record before the network. Read the lookup table note next if stage four is what is limiting you, or the retry note if stage seven is.

Questions this note gets asked

What is the difference between a transaction and an instruction on Solana?

An instruction is a single call to a program with its accounts and data; a transaction is an ordered list of instructions signed together, executed atomically, and subject to one shared compute budget and one size limit. That atomicity is why a swap plus its compute budget settings plus an account creation can be one unit that either fully happens or does not.

Why does a transaction expire?

Because it references a recent blockhash, and validators only accept blockhashes from a recent window, roughly the last 150 blocks. The mechanism prevents indefinite replay of old signed transactions. The practical consequence for an engine is that every built transaction carries a deadline, and the deadline starts running at build time rather than at submission time.

Should the builder or the sender fetch the blockhash?

The builder, because the blockhash is part of the message being signed and the deadline derived from it belongs with the artefact. If the sender fetches it, the sender is effectively rebuilding, which breaks the property that a retry can resend identical bytes. Keeping it in the builder is what makes safe retries possible at all.

Is simulation reliable?

It is reliable about the state it ran against, which is not necessarily the state your transaction will meet. Simulation catches missing accounts, wrong program arguments, insufficient balance and compute overruns, all of which are worth catching. It cannot promise the pool has the same price a second later, so treat it as a correctness gate rather than an outcome prediction.

What does confirmed actually mean?

It depends which commitment level you asked for. Processed means a validator has seen it, confirmed means it has supermajority votes, and finalized means it is effectively irreversible. An engine that records outcomes at processed will occasionally record something that never finalises, which is why accounting should use a stricter level than the user interface does.

Can two identical swaps produce different transactions?

They should not, given the same intent, lease and route. If they do, something non-deterministic has leaked into the builder, usually a timestamp, a random ordering of accounts, or a freshly fetched quote. That non-determinism is the reason some engines cannot safely resend, because they have no stable artefact to resend.

Filed under Pipeline by The Engine Room Desk. Arithmetic on this page is labelled illustrative and built from protocol constants or values you supply yourself. How the desk sources and corrects a note is set out in the editorial policy.