Compute units: the limit that silently fails your transaction
Every Solana transaction gets a compute budget, and exceeding it fails the transaction on chain after you have already paid for it. This covers how compute units are metered, how to measure what your instruction actually consumes, and why requesting the maximum is the wrong default.
- Every transaction gets a compute budget. Exceeding it fails the transaction on chain, after you have paid.
- The default is 200,000 units per instruction and the maximum is 1,400,000 per transaction.
- The limit you request is what the scheduler prices you at, so an inflated request costs real money on every send.
- Simulate with the limit at maximum, read unitsConsumed, add 10 to 20 percent headroom. Do not guess.
Compute units are the least glamorous part of a Solana transaction and one of the highest-leverage. Getting them wrong fails transactions you have already paid for, or quietly multiplies your priority fee by a factor of ten on every single send.
Both of those are worth an afternoon to fix.
What a compute unit is#
The Solana runtime meters execution. Every instruction a program runs costs units: arithmetic, memory access, cross-program invocations, signature verification, account loading. When a transaction exhausts its budget, the runtime aborts it.
Two properties matter and they are easy to state:
- It is deterministic. The same instructions against the same account state consume the same units, every time. That is what makes measurement worthwhile.
- Exceeding it is an on-chain failure. The transaction lands, aborts, and charges you. This is the one item on the standard failure list that is not a delivery problem.
The limits that apply#
| Limit | Value | Notes |
|---|---|---|
| Default per instruction | 200,000 | Applied when you set no limit |
| Maximum per transaction | 1,400,000 | The hard ceiling you can request |
| Maximum per account per block | 12,000,000 | Why hot accounts throttle everyone |
| Cost of a budget instruction | 150 each | Small, but include it in your limit |
That third row is worth pausing on. There is a per-account per-block compute ceiling, so a single extremely hot account can saturate its own budget and throttle every transaction touching it regardless of what anyone paid. If your target account is that hot, no fee and no delivery path fixes it, and that is a protocol constraint rather than a tuning problem.
Why the limit costs you money#
The instinct is to request 1.4 million units and never think about it again. That instinct is expensive, for two separate reasons.
It multiplies your priority fee
Your priority fee is units × price ÷ 1,000,000. The units in that formula are the units you requested, not the units you consumed. So at the same competitive bid:
| Requested | Bid | You pay | Versus measured |
|---|---|---|---|
| 1,400,000 | 50,000 µL/CU | 70,000 lamports | 11.7x |
| 400,000 | 50,000 µL/CU | 20,000 lamports | 3.3x |
| 120,000 (measured) | 50,000 µL/CU | 6,000 lamports | 1x |
On a bot sending thousands of transactions a day, that difference is not a rounding error. Measuring compute is a fee optimisation before it is anything else.
It makes you harder to schedule
The leader packs blocks against a compute budget. A transaction claiming 1.4 million units occupies a large slice of that budget in the scheduler’s accounting, whether or not it uses it. A transaction claiming an honest 120,000 fits into gaps that the greedy one does not.
So an inflated limit costs you twice: you pay more per send, and you are marginally harder to fit into a block.
Measuring what you consume#
Simulation reports it. The detail people miss is that you must simulate with the limit already raised, or simulation aborts at the default and you measure your own ceiling.
1import { Connection, VersionedTransaction, PublicKey, TransactionMessage, ComputeBudgetProgram } from "@solana/web3.js";23/**4 * Measure, then set. Never guess.5 *6 * The trick people miss: simulate with the limit already set to the MAXIMUM,7 * otherwise simulation aborts at the default 200k and reports that as the8 * consumption. You are measuring the ceiling you imposed, not the work.9 */10export async function measureUnits(11 rpc: Connection,12 payer: PublicKey,13 instructions: TransactionInstruction[],14 lookupTables: AddressLookupTableAccount[] = [],15): Promise<number> {16 const { blockhash } = await rpc.getLatestBlockhash("confirmed");1718 const probe = new VersionedTransaction(19 new TransactionMessage({20 payerKey: payer,21 recentBlockhash: blockhash,22 instructions: [23 ComputeBudgetProgram.setComputeUnitLimit({ units: 1_400_000 }),24 ...instructions,25 ],26 }).compileToV0Message(lookupTables),27 );2829 const sim = await rpc.simulateTransaction(probe, {30 replaceRecentBlockhash: true,31 sigVerify: false,32 });3334 if (sim.value.err) {35 throw new Error(`simulation failed: ${JSON.stringify(sim.value.err)}\n${sim.value.logs?.join("\n")}`);36 }3738 // Subtract the probe's own budget instruction, which your real transaction39 // will also carry but which you should not double count.40 return sim.value.unitsConsumed ?? 0;41}4243/** Measure once at startup, cache per instruction shape, re-measure on drift. */44export function withHeadroom(measured: number, factor = 1.15) {45 return Math.min(1_400_000, Math.ceil(measured * factor));46}
The same in Rust and Python, since this usually belongs wherever your builder lives.
1from solana.rpc.api import Client2from solana.rpc.types import TxOpts3from solders.compute_budget import set_compute_unit_limit4from solders.message import MessageV05from solders.transaction import VersionedTransaction67MAX_UNITS = 1_400_0008910def measure_units(rpc: Client, payer, instructions, blockhash) -> int:11 """Simulate at the maximum limit so the reported figure is the real work."""12 probe = VersionedTransaction(13 MessageV0.try_compile(14 payer=payer.pubkey(),15 instructions=[set_compute_unit_limit(MAX_UNITS), *instructions],16 address_lookup_table_accounts=[],17 recent_blockhash=blockhash,18 ),19 [payer],20 )2122 sim = rpc.simulate_transaction(probe, sig_verify=False)23 if sim.value.err is not None:24 raise RuntimeError(f"simulation failed: {sim.value.err}")2526 return sim.value.units_consumed or 0272829def with_headroom(measured: int, factor: float = 1.15) -> int:30 return min(MAX_UNITS, int(measured * factor) + 1)
Finding the expensive part
A total is not actionable. Program logs break consumption down per program, which tells you whether the cost is yours to optimise or the AMM’s to bear.
1/**2 * Program logs report consumption per program, which is how you find the3 * expensive part rather than just the expensive total.4 *5 * Look for lines of this shape in simulation output:6 *7 * Program <id> consumed 34918 of 1400000 compute units8 *9 * A swap that consumes 180k total might be 40k of your own logic and 140k in10 * the AMM. Knowing which is which decides whether optimising is even possible.11 */12export function parseConsumption(logs: string[]): { program: string; units: number }[] {13 const pattern = /^Program (\S+) consumed (\d+) of \d+ compute units$/;1415 return logs16 .map((line) => line.match(pattern))17 .filter((m): m is RegExpMatchArray => m !== null)18 .map((m) => ({ program: m[1], units: Number(m[2]) }))19 .sort((a, b) => b.units - a.units);20}2122// Usage:23// const sim = await rpc.simulateTransaction(tx, { sigVerify: false });24// console.table(parseConsumption(sim.value.logs ?? []));
Choosing headroom#
Consumption is deterministic against fixed state, but state moves between your simulation and your execution. A swap crossing one more tick, an account that now needs initialising, a slightly different route. So you need headroom, and the right amount depends on how much the state can drift.
| Transaction shape | Headroom | Why |
|---|---|---|
| Plain SOL or SPL transfer | 5% | Fixed cost, essentially no drift |
| AMM swap, constant product | 10 to 15% | Modest state dependence |
| CLMM swap crossing ticks | 25 to 40% | Tick crossings are unpredictable |
| Multi-hop route | 30%+ | Compounds across every hop |
| May create accounts | Measure both paths | Creation is a step change, not a margin |
Typical consumption#
Rough figures for orientation only. Measure your own; these move with program versions and account state.
| Operation | Approximate units |
|---|---|
| SOL transfer | ~450 |
| SPL token transfer | ~4,500 |
| Create an associated token account | ~25,000 |
| Raydium AMM v4 swap | ~30,000 to 60,000 |
| Raydium CLMM swap | ~60,000 to 250,000 |
| Orca Whirlpool swap | ~50,000 to 180,000 |
| pump.fun buy | ~35,000 to 80,000 |
| Jupiter route, two hops | ~150,000 to 400,000 |
The concentrated liquidity ranges are wide for a real reason: a swap that crosses many initialised ticks does materially more work than one that stays inside a single tick array. That is the case that needs generous headroom.
Reducing consumption#
Lower consumption means a cheaper fee at the same bid and an easier fit in a block.
- Do not mark accounts writable that you only read. It costs units and it invents contention you did not need. Free win, twice over.
- Use address lookup tables. They shrink the transaction and reduce account loading cost. On a route with many accounts this is substantial, and it may be the difference between fitting in 1,232 bytes and not.
- Pre-create token accounts. Creating one inline costs about 25,000 units on the critical path. Create it once, in advance, and never pay it again.
- Split unrelated work. Two transactions each doing one thing consume less in total than one doing both, and they can be scheduled independently.
- Prefer simpler routes when competing. A direct pool swap consumes a fraction of a multi-hop route. When you are racing, the simpler route often wins on delivery even at a slightly worse quoted price.
Reading the failure#
When you exceed the budget you get this, on chain, having paid:
1Program <id> consumed 200000 of 200000 compute units2Program <id> failed: exceeded CUs meter at BPF instruction34// In getSignatureStatuses:5// err: { InstructionError: [2, "ComputeBudgetExceeded"] }
The index in InstructionError tells you which instruction ran out, which combined with the per program log breakdown usually identifies the cause immediately.
Once your compute limits are measured, revisit your fee. The two interact directly, and a correctly measured limit usually means you can bid a higher percentile for the same money. That maths is in the priority fees guide.