Skip to content

The Compute Budget and Priority Fees

The last five pages built a transaction from the outside in: its anatomy, its instructions and access flags, the message and recent blockhash that pin it in time, and the versioned form with lookup tables that lets it name more accounts. All of that describes what a transaction touches. This page is about how much it is allowed to cost while it runs, and how it competes for the scarce resource that costs the most.

A validator is a machine with finite CPU and a hard deadline: it has roughly 400 milliseconds to produce a block (why 400ms) and then it must move on. A transaction that looped forever, or merely ran for a long time, would blow that deadline and stall the whole chain. So every transaction runs under a compute budget — a hard ceiling on how much work it may do — and can attach a priority fee to bid for a place in the block. Both ideas fall straight out of the throughline: how do you build a single global state machine that runs at hardware speed without falling apart? You cap the work so no one transaction can hang the machine, and you price the contention so the machine schedules fairly under load.

Compute units: metering as a halting-problem defense

Section titled “Compute units: metering as a halting-problem defense”

You cannot, in general, look at an arbitrary program and decide whether it will ever stop — that is the halting problem, and it is undecidable. A validator can’t afford to find out the hard way. So instead of proving a program terminates, Solana forces it to: it charges every operation a fixed number of compute units (CU) and aborts the transaction the instant the running total crosses a limit.

  • Each low-level operation the runtime performs — an arithmetic op in the BPF virtual machine, a syscall, a cross-program invocation, loading an account — has a fixed CU cost.
  • The runtime keeps a running counter as the transaction executes. Every step adds its cost.
  • The moment the counter exceeds the transaction’s CU limit, execution stops and the transaction fails with no state change committed (the same clone-then-commit-on-success shape the runtime already uses to roll back failed work — parallel execution).
compute budget = a fuel gauge for one transaction
─────────────────────────────────────────────────
CU spent: [■■■■■■■■□□□□□□□□] limit = 200,000
^ each op burns a fixed amount
crosses the limit ──► ABORT, no state committed
never crosses ──► runs to completion, unused CU is just discarded

This is Solana’s version of what Ethereum calls gas, and the metering half is nearly identical: both assign a fixed cost to each primitive operation and halt a transaction that runs out. It is a defense, not an accounting nicety. A malicious contract cannot hang a validator by looping, because the fuel gauge hits zero and the runtime kills it. Termination is guaranteed by construction rather than proven.

By default, each transaction is granted a fixed CU allowance — historically 200,000 CU per instruction, capped at a per-transaction maximum (on the order of 1.4 million CU as of 2024; treat the exact figures as validator-configurable and subject to change). A block also has a total CU cap across all its transactions, so the sum of everyone’s budgets can’t exceed what a validator can chew through in its slot.

That gives us three nested ceilings, each answering “falling apart” at a different scale:

per instruction ──► one instruction can't run away
per transaction ──► one transaction can't monopolize a core
per block ──► one block can't blow the 400ms deadline

The default is deliberately modest. A simple transfer costs a few thousand CU; a heavy DeFi transaction chaining several cross-program invocations can want far more than 200,000. That is where the transaction gets to ask.

The ComputeBudget program: asking for more, bidding for priority

Section titled “The ComputeBudget program: asking for more, bidding for priority”

The compute budget isn’t fixed policy imposed from outside — it’s set by instructions inside the transaction itself, handled by a native program called ComputeBudget. You prepend one or two of its instructions to your transaction, exactly like any other instruction, and they configure the budget for the whole transaction before your real instructions run.

Two of its instructions matter here:

  • SetComputeUnitLimit(u32) — request a specific CU ceiling for this transaction, up or down from the default. Ask for 400,000 if your DeFi call needs it; ask for 10,000 if you’re doing something cheap and want to be a good citizen.
  • SetComputeUnitPrice(u64) — bid a price in micro-lamports per CU. This is the priority fee knob: it tells the scheduler how much you’re willing to pay, per unit of compute, to be included ahead of others competing for the same resources. (A micro-lamport is 10⁻⁶ lamports, and a lamport is 10⁻⁹ SOL, so the unit is deliberately tiny — priority fees are meant to be a fraction of a cent in the common case.)

Here is what building that looks like against a typical Solana client (the two ComputeBudget instructions go first, then your program’s instruction):

use solana_sdk::compute_budget::ComputeBudgetInstruction;
// 1. Ask for the CU ceiling this transaction actually needs.
let set_limit = ComputeBudgetInstruction::set_compute_unit_limit(300_000);
// 2. Bid a priority fee: micro-lamports PER compute unit.
let set_price = ComputeBudgetInstruction::set_compute_unit_price(1_000);
// 3. Your real instruction(s) follow.
let ix = my_program_instruction(/* accounts, data */);
// Order matters only in that the budget instructions configure the whole tx;
// they are conventionally placed first.
let message = Message::new(&[set_limit, set_price, ix], Some(&payer.pubkey()));

Note what each instruction does not do. SetComputeUnitLimit does not make your transaction run faster or buy priority — it only changes the ceiling. SetComputeUnitPrice does not raise the ceiling — it only sets the bid. They are orthogonal knobs, and you almost always want to set both.

The priority fee is limit × price — so requesting the limit accurately matters

Section titled “The priority fee is limit × price — so requesting the limit accurately matters”

Your priority fee — the extra you pay on top of the near-zero base fee to jump the queue — is simply:

priority fee (lamports) = compute_unit_limit × compute_unit_price(µLamports/CU) ÷ 1,000,000

That single formula is why requesting the limit accurately is a real decision, not a formality. The limit you request is what you’re billed against for prioritization, and it’s the space the scheduler reserves for you in the block. Both directions of getting it wrong hurt:

  • Over-request and you waste block space. The scheduler reserves your requested CU against the block’s total cap, so asking for 1,400,000 CU when you’ll use 40,000 tells the validator to hold room it can’t give to anyone else — fewer transactions fit, and you may have paid a higher priority fee than you needed to. You’ve made the block worse for everyone, including yourself.
  • Under-request and you fail mid-execution. If you ask for 50,000 CU but your instruction actually needs 120,000, the fuel gauge hits your self-imposed limit partway through, the runtime aborts, and no state is committed. You still paid the base fee for the attempt, and you got nothing.
request too HIGH ──► reserves block space you don't use ──► fewer txs fit, you overpay
request just RIGHT ──► fits tightly, pay for what you use
request too LOW ──► fuel runs out mid-run ──► ABORT, base fee burned, no effect

The disciplined move is to simulate the transaction first to learn its actual CU consumption (an RPC call returns the units a run would burn), then request that number plus a small safety margin. Then set the price separately based on how badly you need to win the current contention.

Priority fees bid on accounts, not the whole chain — local fee markets

Section titled “Priority fees bid on accounts, not the whole chain — local fee markets”

Here is where the compute budget stops being a copy of Ethereum’s gas and becomes something the earlier parts of this book predicted. On Ethereum, gas price is a global auction: every transaction in the mempool competes on one price, so when one popular contract gets hot, everyone’s fees rise. A busy NFT mint prices out an unrelated token transfer, because they bid in the same global market.

Solana’s prioritization is local. Recall the whole reason transactions declare their writable accounts up front (contract-owned storage vs external accounts): so the scheduler can run non-conflicting transactions in parallel and serialize only those that fight over the same writable account. Contention is therefore per writable account. And so is the fee market. When you bid a priority fee, you are bidding for a slot in the queue of transactions contending for the specific writable accounts your transaction touches — not for the chain as a whole.

Ethereum: ONE global gas auction
────────────────────────────────
NFT mint (hot) ─┐
token transfer ─┼─► same price curve ─► hot demand raises EVERYONE's fee
random swap ─┘
Solana: MANY local auctions, one per hot writable account
────────────────────────────────────────────────────────
contend for account M (hot NFT) ─► bid in M's local market (fees spike HERE)
contend for account A (your DEX) ─► bid in A's local market (undisturbed)
contend for account B (a wallet) ─► bid in B's local market (undisturbed)

This is the same insight as the parallel scheduler, viewed from the fee side. Because a hot NFT mint only serializes transactions that write its account, only those transactions compete for its limited serial throughput, and only their priority fees rise. A wallet-to- wallet transfer that shares no writable account with the mint sits in an entirely separate local market and is unaffected. The parallelism that keeps the machine fast is exactly the thing that keeps the fee market localized. One design decision buys both.

Under the hood — why unused compute is simply discarded

Section titled “Under the hood — why unused compute is simply discarded”

One detail trips people who come from a strict-accounting mindset: on Solana you are billed for the CU limit you requested for prioritization purposes, not only the CU you actually burned. If you request 200,000 and use 40,000, the leftover 160,000 isn’t refunded to you as priority fee — it’s just gone, and worse, it was space reserved against the block cap that no one else could use. This is deliberate. Refunding unused compute would let a transaction cheaply reserve a large slice of the block “just in case” and pay only for what it used, defeating the point of the reservation — the whole reason the limit exists is to let the scheduler pack the block before running anything, exactly as it packs accounts before running anything. So the incentive is aligned by making the reservation cost real: request what you need, because slack is pure waste. Simulate, size tightly, and the system rewards you with a lower fee and a better chance of landing in the block.

It’s worth stating the two-model comparison cleanly, because the surface similarity hides a deep difference.

Ethereum gasSolana compute units
MeteringEach opcode has a fixed gas cost; run out and the tx revertsEach op has a fixed CU cost; cross the limit and the tx aborts — identical idea
Base costBase fee floats with block fullness (EIP-1559), can be largeBase fee is flat and near-zero (≈5,000 lamports/signature as of 2024), mostly burned
PrioritizationOne global gas auction; hot demand raises everyone’s feeLocal fee markets; you bid only against transactions contending for your writable accounts
What sets itGas limit + gas price fields on the txSetComputeUnitLimit + SetComputeUnitPrice ComputeBudget instructions

The metering row is the same engineering answer to the same problem — bound the work so no transaction can hang the machine. Everything below it diverges because Solana externalized state and declared footprints, and that one choice turned a global auction into a set of local ones and let the base fee fall to almost nothing.

The compute budget is a major mechanism — Solana’s whole answer to “bound the work and price the contention” — so view it through the five questions.

  • Why does it exist? Because a validator has a hard 400ms deadline and cannot solve the halting problem; metering every operation and aborting at a limit forces termination instead of trying to prove it, keeping any one transaction from hanging the machine.
  • What problem does it solve? Two at once: runaway/expensive transactions (bounded by the CU limit) and fair scheduling under contention (the priority fee lets the scheduler order competing transactions by willingness to pay, per hot account).
  • What are the trade-offs? You must size the limit yourself — over-request and you waste block space and overpay, under-request and you fail mid-execution — so accurate simulation becomes part of building a correct transaction, not an optimization.
  • When should I avoid it? You never remove metering — it’s mandatory. But you can skip a priority fee (leave the price at zero) when the network is quiet and you’re not contending for a hot account; paying to jump an empty queue is pure waste.
  • What breaks if I remove it? Remove the CU limit and a single looping transaction hangs a validator and stalls the chain — the halting problem walks back in. Remove local pricing and hot-account demand has no release valve, so congestion spills chain-wide as failed transactions instead of a localized fee spike.
  1. Compute units are described as “Solana’s answer to the halting problem.” Explain what that means: what can’t a validator do in general, and how does CU metering sidestep it?
  2. Name the two ComputeBudget instructions and state precisely what each one changes — and, just as important, what each one does not change.
  3. Write the priority-fee formula, then explain why both over-requesting and under-requesting the CU limit are costly. What is the disciplined way to pick the number?
  4. Solana’s fee market is called local. Local to what, and why does that follow directly from the account model and the parallel scheduler built in earlier parts?
  5. Compute units and Ethereum gas share one row of the comparison table exactly and diverge on the rest. Which row is the same, and name two ways they differ.
Show answers
  1. In general you cannot decide whether an arbitrary program will ever halt (the halting problem is undecidable), so a validator can’t prove a transaction terminates. CU metering sidesteps this by charging every operation a fixed number of units and aborting the moment a running counter crosses the limit — termination is forced by construction rather than proven, so no transaction can loop forever and hang the validator inside its 400ms slot.
  2. SetComputeUnitLimit requests a specific CU ceiling for the transaction (up or down from the default) — it does not buy priority or make the tx faster. SetComputeUnitPrice bids a priority fee in micro-lamports per CU — it does not raise the ceiling. They are orthogonal, and you usually set both.
  3. priority fee = compute_unit_limit × compute_unit_price(µLamports/CU) ÷ 1,000,000. Over-request reserves block space you won’t use against the block’s CU cap (fewer txs fit, you likely overpay); under-request means the fuel gauge hits your self-imposed limit mid-execution, so the tx aborts with no state committed and the base fee wasted. The disciplined approach is to simulate the transaction to learn its real CU consumption, then request that plus a small margin, and set the price separately based on contention.
  4. Local to the specific writable accounts the transaction contends for. Because Solana transactions declare their writable accounts up front, the scheduler serializes only those that fight over the same writable account, so contention is per-account — and the fee market is scoped the same way: you bid only against transactions competing for your hot accounts, so a hot NFT mint’s fee spike doesn’t touch an unrelated wallet transfer.
  5. The metering row is identical — both assign a fixed cost to each primitive operation and halt a transaction that exceeds its limit. They differ in (a) the base fee: Ethereum’s floats with block fullness and can be large, while Solana’s is flat and near-zero (≈5,000 lamports/signature as of 2024); and (b) prioritization: Ethereum runs one global gas auction, while Solana runs local per-account fee markets. (Also acceptable: what sets the budget — tx gas fields vs ComputeBudget instructions.)