Compute Units and Limits
The loader and verifier proved a program’s bytecode is structurally safe before it ever runs, and the syscall boundary proved it can’t reach outside its sandbox while running. Both are static or boundary checks. Neither answers the one question a fast, looping virtual machine leaves open: how long will this program run? An SBF program can loop. A verified, sandboxed loop that never terminates is still a catastrophe — it would pin a core on every validator forever, and the whole network would stall waiting on one transaction.
This page is about the runtime’s answer to that: compute units. Every SBF instruction and every syscall costs a fixed number of units; each transaction is handed a bounded budget; and the instant the program spends past it, execution aborts and the transaction fails. It is the same move Ethereum made with gas — turning the undecidable question “will this halt?” into the decidable question “will this halt before it runs out of budget?” — but Solana meters compute directly, in units of work rather than in a per-op price, and it caps that budget per transaction and per block so the scheduler can reason about how much work fits in a slot.
The halting problem, one more time
Section titled “The halting problem, one more time”You cannot, in general, decide whether an arbitrary program halts. That is the halting problem, and it is not a Solana problem or an Ethereum problem — it is a theorem. Any chain that lets programs loop inherits it. Bitcoin Script sidesteps it by forbidding loops entirely: a Script that can’t jump backwards always terminates, but a language that can’t loop is a weak language.
Solana, like the EVM, wants the strong language — real loops, real control flow — so it can’t dodge the theorem. Instead it bounds the problem. You don’t need to know whether a program halts; you only need to guarantee it halts within a fixed amount of work, and that you can decide trivially: count the work as you go, and stop when the counter hits the ceiling.
undecidable: "does this program halt?" ← the halting problem (a theorem; no) decidable: "does this program halt within N units?" ← run it, count to N, stop at N (yes)That single substitution is the whole idea. A metered VM never has to answer the impossible question. It answers the possible one, every instruction, by decrementing a budget.
Compute units: metering the work directly
Section titled “Compute units: metering the work directly”A compute unit (CU) is Solana’s abstract unit of computational work. Every SBF instruction the VM executes costs some CUs, and every syscall — a log, a hash, a signature check, a cross-program call — costs a larger, fixed number that reflects its real expense. As a transaction executes, the runtime deducts the cost of each operation from a running budget. Two things can happen:
- It finishes within budget → its account writes commit, and the transaction succeeds.
- It runs out first → execution aborts immediately, and — exactly as in the parallel runtime’s clone-then-commit model — none of its account changes are written back. The transaction fails as if it never touched state.
budget: 200,000 CU (a per-transaction default)
instruction / syscall cost remaining ───────────────────────── ───── ───────── ... arithmetic, branches ~1 each 199,993 sol_log_ syscall ~100 199,893 sol_sha256 syscall ~ per byte ... loop iteration × N ~k each decreasing ... └─ hits 0 → ABORT, roll backThe number to hold onto: a transaction’s compute budget is bounded, and it is spent, not borrowed. There is no way for a program to run “just a little longer” once the counter reaches zero. An infinite loop doesn’t hang the validator — it burns its budget at a few CUs per iteration, hits the ceiling in a predictable, finite number of steps, and dies. The validator moves on to the next transaction. The looping VM is safe precisely because looping is expensive and finite, never free and unbounded.
Where the limits come from
Section titled “Where the limits come from”There isn’t one budget; there is a small hierarchy of them, and each exists to protect a different thing.
- Per-transaction limit. A transaction gets a default compute budget (historically 200,000 CU), and it may request more up to a hard per-transaction maximum (on the order of 1.4 million CU) via a Compute Budget instruction. This ceiling is what makes a single transaction’s cost knowable in advance.
- Per-block (per-slot) limit. All the transactions in a block share a total compute ceiling. A block is a fixed slice of wall-clock time (a slot), and a validator can only do so much work in that time. The block limit is the runtime’s promise that a slot’s total work fits in a slot’s duration.
- Per-account write limit. There is also a cap on how much compute can be spent on transactions that write the same account in one block — the metering counterpart to the hot-account contention the parallel scheduler already has to serialize.
┌─────────────────────────── per-block CU limit ───────────────────────────┐ │ tx tx tx tx ... (each ≤ per-tx max; requested budget declared) │ │ └── per-account write CU limit caps work on any one hot account ──┘ │ └───────────────────────────────────────────────────────────────────────────┘Every one of these is a declared, bounded number. That is not an accident — it is the property the rest of the runtime is built on, as we’ll see below.
Contrast with EVM gas: same trick, different economics
Section titled “Contrast with EVM gas: same trick, different economics”Ethereum solved the identical halting problem with gas: every EVM opcode has a gas price, the transaction carries a gas limit, and running out reverts the transaction. Solana’s compute units are the same halting-problem-as-economics move. But two differences matter, and they’re both consequences of Solana’s throughput goal.
Solana meters work; the EVM prices it. A compute unit is a measure of work — how much of a slot’s
capacity this operation consumes. Gas is fused with price: fee = gas used × gas price, so on Ethereum
the metering unit is also the thing you pay for, op by op. On Solana the two are deliberately separated.
CUs bound and schedule work; the fee is a mostly separate, near-flat number.
Base fees are cheap and roughly flat. A Solana transaction’s base fee is a small, near-constant amount per signature — it does not scale up with every operation the way gas does. Compute units don’t set the primary price of a transaction; they bound its work and feed the priority-fee / local-fee-market system. You pay a priority fee — measured per compute unit requested — to bid for scarce blockspace or for the limited serial throughput of a hot account. So CUs on Solana are closer to a scheduling budget than to a price tag.
Ethereum: gas = metering AND price fee = gas_used × gas_price (op cost ≈ your bill) Solana: CU = metering (work/budget) base_fee ≈ flat per signature priority_fee = (CU requested) × (price you bid) → local fee marketThe throughline connection: Ethereum’s design optimizes for a single global ordering where every op you pay for is the price of consensus. Solana’s design optimizes for throughput, so it wants the cost of a transaction to be a declared, scheduling-friendly budget first, and a market signal second. Metering directly — in units of work, capped per transaction and per block — is exactly what a runtime built for speed needs.
Under the hood — why declared cost is what makes the scheduler work
Section titled “Under the hood — why declared cost is what makes the scheduler work”Compute limits are not just a safety valve; they are load-bearing for Solana’s whole parallelism story. The Sealevel scheduler packs many transactions into a slot and runs the non-conflicting ones across cores. To do that it must answer a question before execution: how much of this slot’s capacity will this transaction consume?
If a transaction could cost an unbounded, unknown amount of compute, the scheduler could not fit work into a block at all — it would have no idea whether the slot’s budget was spent. Because every transaction carries a declared, bounded compute budget, the scheduler can treat blockspace as a knapsack: add transactions until the block’s CU limit is reached, and stop.
without bounded cost: "how many txs fit in this slot?" → unknowable → can't schedule with bounded cost: block_CU_limit / Σ(declared tx budgets) → a packing problem it can solveThis is why declaring an accurate compute budget is a cooperative act, not just a defensive one. A transaction that requests a smaller, honest budget is easier to pack and gets scheduled sooner; one that over-requests wastes reservable blockspace. Bounded, declared cost per transaction is the same enabling constraint as declared account footprints: both let the runtime reason about a transaction before running it, which is the only way to schedule a global state machine at hardware speed.
What actually happens when the budget runs out
Section titled “What actually happens when the budget runs out”Walk the failure carefully, because the guarantees hide in the details. Suppose a program hits an infinite loop, or simply asks for more than its budget allows:
- The VM executes instructions, decrementing the budget each step.
- The budget reaches zero mid-execution.
- The runtime aborts the program with a compute-exhaustion error. No further instruction runs.
- Because Solana runs each transaction against a private working copy of its accounts and commits only on success, the aborted transaction’s writes are simply discarded — the clone-then-commit rollback you already know.
- The transaction is recorded as failed. The validator moves to the next one, having spent only a bounded amount of work on this one.
loop { ... } ← would run forever
reality: each iteration deducts CUs → budget hits 0 → ABORT → account changes rolled back → tx fails → validator continuesThree properties fall out, and they’re the point of the whole mechanism:
- The network never hangs. The worst an infinite loop can do is waste its own bounded budget. Finite work, then dead.
- Failure is clean. A budget-exhausted transaction leaves no partial state — it’s atomic, same as any other failure in the runtime.
- Cost is predictable. The maximum work any transaction can extract from a validator is its requested budget, capped by the per-transaction maximum. There is no unbounded case to defend against.
The architect’s lens
Section titled “The architect’s lens”- Why does it exist? Because a virtual machine that allows loops inherits the halting problem, and a global state machine cannot let one transaction pin every validator’s core forever. Compute units bound execution so “will it halt?” becomes “will it halt within budget?” — a question you can always answer.
- What problem does it solve? It makes a fast, looping VM safe. It turns unbounded, unknowable execution cost into a declared, bounded budget, which simultaneously prevents denial-of-service by infinite loops and lets the scheduler pack a slot’s worth of work.
- What are the trade-offs? Programs must fit real work inside a fixed budget, so developers optimize CU usage and sometimes request more budget or split work across transactions. The per-account write cap and hard per-transaction ceiling mean some workloads simply cannot be done in one transaction at any price — money buys priority, not more compute.
- When should I avoid it? You don’t — it’s not optional. But you design around it: keep instructions cheap, avoid unbounded loops over account data, and don’t rely on a single hot account when contention for its bounded compute would serialize and cap your throughput.
- What breaks if I remove it? Everything. One malicious or buggy program with an infinite loop hangs every validator, halting the chain. And even without malice, the scheduler loses the declared cost it needs to fit transactions into a slot, so parallel block packing becomes impossible. Compute metering is what lets the runtime run at hardware speed without falling apart.
Check your understanding
Section titled “Check your understanding”- The halting problem says you can’t decide whether an arbitrary program halts. How do compute units let Solana allow real loops anyway without ever having to answer that question?
- A transaction hits an infinite loop. Walk through exactly what happens to the transaction, to its account changes, and to the validator — and explain why the network doesn’t hang.
- Both Solana CUs and EVM gas solve the same halting-problem-as-economics move, yet they’re economically different. What does Solana meter directly that the EVM fuses with price, and how does that show up in base fees versus priority fees?
- Why is a transaction’s bounded, declared compute budget essential to the parallel scheduler, not just to safety? What could the scheduler not do without it?
- During a hot NFT mint, transactions targeting one account congest the network. Connect this to compute limits and to local fee markets: what caps the work on one account, and why is the fee market scoped per-account too?
Show answers
- It replaces the undecidable question “does this program halt?” with the decidable one “does it halt within N compute units?” Every instruction and syscall deducts from a bounded per-transaction budget; the runtime just counts and stops at zero. A loop is allowed because it is finite and expensive — it burns its budget in a predictable number of iterations and then aborts, so the impossible question never has to be answered.
- The VM executes the loop, deducting CUs each iteration, until the budget reaches zero. The runtime then aborts with a compute-exhaustion error; no further instruction runs. Because each transaction runs against a private working copy of its accounts and commits only on success, the aborted transaction’s writes are discarded (clean rollback), and it’s recorded as failed. The validator moves on, having spent only the bounded budget — so an infinite loop wastes finite work and dies instead of hanging the network.
- Solana meters work directly: a CU measures how much of a slot’s capacity an operation consumes, and
that’s separate from price. The EVM fuses metering and price (
fee = gas used × gas price), so each op you pay for is your bill. On Solana the base fee is small and near-flat (roughly per signature), while CUs feed the priority fee (bid per CU requested) and local fee markets. So CUs are a scheduling budget first, a market signal second. - To pack a block, the scheduler must know before execution how much of the slot’s compute a transaction will consume. A bounded, declared budget turns block packing into a solvable knapsack: add transactions until the block’s CU limit is reached. Without it, cost would be unknowable, the scheduler couldn’t tell whether a slot’s budget was spent, and it couldn’t fit parallel work into a slot at all.
- Transactions writing the same account can’t parallelize, so the scheduler serializes them and they compete for that account’s limited share of block compute; a per-account write CU limit caps how much work any one hot account can consume in a block. Because contention is per-writable-account, the fix is per-account too: local fee markets and priority fees (bid per CU) localize the bidding for that hot account’s scarce serial throughput instead of pricing the whole chain.