LAT-02
Compute Units and Limits: Sizing a Transaction Budget
The compute unit limit is the least glamorous setting in a Solana transaction and one of the few that changes both what you pay and whether you fit. Left at its default it is usually too generous; guessed downward it reverts the transaction and charges for the privilege.
Fault card
- Symptom
- Transactions revert with an exhausted budget, or cost more than they should
- Mechanism
- Requested units drive both the fee and the space a leader must reserve
- Instrument
- simulateTransaction unitsConsumed, getTransaction consumed units
- Correction
- Measure consumption, add a stated margin, re-measure when the route changes
A compute unit is the Solana runtime's meter for work done during execution. Every transaction carries a budget in units, and a compute budget instruction lets you set it explicitly. That single number decides two things at once: how much priority fee you pay, and how much room a leader must reserve to include you.
What a compute unit is
Units are charged for the operations a program performs: instruction dispatch, account access, cross-program invocation, cryptographic work and the ordinary cost of running compiled code. The exact schedule is a runtime detail that changes as the network evolves, so the useful discipline is to measure consumption rather than to predict it from a table.
What does not change is the shape of the rule. Execution proceeds until either the instructions complete or the budget is exhausted. If the budget runs out the whole transaction reverts, and because it was included in a block it pays. There is no partial execution and no partial refund.
That makes the limit a correctness setting rather than an optimisation. Most settings in a send path trade one good property for another; this one has a wrong side. Below actual consumption it produces guaranteed failures, and the cost of those failures is charged in full.
Defaults and ceilings
Two numbers frame the decision. Without an explicit instruction, the runtime applies a default allowance per instruction. And regardless of what is requested, a single transaction may not exceed 1,400,000 units, which is the hard per-transaction ceiling.
The default is where the money leaks. A simple swap consumes far less than the default allowance for a multi-instruction transaction, and a priority fee attached to an un-tightened default multiplies the price by a request that bears no relation to the work. Setting an explicit limit is therefore the first fee optimisation available, and it is the only one that costs nothing in competitiveness.
The ceiling matters at the other end. A transaction that genuinely needs more than 1,400,000 units cannot exist, so a route that grows past it has to be split into two transactions, which changes the atomicity properties of the operation. That is a design decision rather than a tuning one, and discovering it late is unpleasant. The published fee and compute documentation is the reference to check when a build starts approaching it.
Limits above the transaction
The transaction ceiling is not the only ceiling. A block has a total compute capacity, and there is a separate, lower ceiling on how many units may be spent writing any single account within one block. The second one is the reason a busy pool can become unusable while the network as a whole looks fine.
The exact values have been raised more than once through network change proposals, so quoting a current figure invites a page that ages badly. The mechanism is what matters and it is stable: block capacity is finite, per-account write capacity is a smaller slice of it, and both are enforced by the leader as it packs.
For an operator the practical reading is that fee escalation has a hard stop. When a hot account has reached its per-block write ceiling, no price buys inclusion in that block, because there is no capacity left to buy. Transactions aimed at that account queue for a later block, and the only levers available are writing to a different account, sending fewer transactions at once, or waiting for the pressure to pass.
Why the limit is two decisions
The requested limit is used twice by two different consumers, and they pull in the same direction, which is convenient.
| Consumer | What it does with the request | Effect of over-requesting | Effect of under-requesting |
|---|---|---|---|
| Fee calculation | Multiplies the request by the unit price | Pays for units never used | Cheaper, until the transaction reverts |
| Leader scheduling | Reserves the request against block capacity | Harder to fit into a nearly full block | Easier to fit |
| Runtime execution | Stops execution when the budget is exhausted | No effect; unused budget is simply unused | Reverts, having landed and paid |
Because the first two rows both punish over-requesting, tightening the limit improves cost and schedulability at the same time. That is unusual and worth exploiting. The counterweight is entirely in the third row, and it is severe enough that the margin above measured consumption should never be zero.
Measuring consumption properly
Simulation reports units consumed for the run it performed. A landed transaction reports units consumed for the run that actually happened. Both are real measurements and neither is the whole picture, because consumption varies with the state the transaction meets.
The right object to build is a small distribution rather than a single value. Simulate the same intent shape several times across different market conditions, collect the consumed figures from landed transactions of the same shape, and look at the spread. A limit set from one sample on a quiet afternoon will be tight in exactly the conditions where reverting is most expensive.
One caution about simulation: it does not run under identical conditions to execution. Accounts may need creating in one case and not the other, and a first write to an account can cost more than a subsequent one. Those differences are not noise, they are structural, and they belong in the margin decision rather than being averaged away.
Where the units go
A single transaction is rarely one thing. A swap message typically carries compute budget instructions, possibly an account creation, the swap itself and sometimes a cleanup. Each contributes to the total, and knowing roughly where the weight sits is what makes a limit adjustment targeted rather than a shrug.
The compute budget instructions themselves are cheap, which is worth saying because operators occasionally wonder whether adding them is self-defeating. They are small, fixed and negligible against anything else in the message. There is no version of the arithmetic in which omitting them to save units is the better choice.
The heavy items are the ones that touch state. Creating an associated token account is a well-known step change: the transaction that creates it costs materially more than the ones that follow, and a build that sometimes creates and sometimes does not will show a bimodal consumption distribution. Treating that distribution as one population and setting a limit at its average guarantees that the creating transactions revert.
| Component | Weight | Varies with |
|---|---|---|
| Compute budget instructions | Negligible | Nothing |
| Token account creation | Substantial, once per account | Whether the account already exists |
| Swap instruction | The bulk of the transaction | Venue, pool type, route length |
| Cross-program invocations | Adds with depth | How many programs the route touches |
| Account writes | Scales with the account list | How many accounts the message declares writable |
The correct handling of a bimodal distribution is to split the population rather than to widen the margin until it covers both. Build the creating case as its own shape with its own limit, and the ordinary case with a tighter one. That costs a branch in the builder and saves the difference on every transaction that does not need the larger budget, which is most of them.
The same logic applies to venues. If a run trades across several pool types, their consumption profiles are different populations and deserve different limits. One global limit sized for the most expensive venue overpays on every transaction sent to the cheapest one, and across a long run that is a real number rather than a rounding error.
What makes consumption drift
A limit that was correct last week can be wrong today without a single line of your code changing. The causes are worth knowing because they are all detectable in advance.
| Cause | Direction | How to detect it early |
|---|---|---|
| Route gained an extra hop | Up, sometimes sharply | Compare instruction count between builds |
| Token account has to be created | Up for that transaction only | Check account existence before building |
| Venue changed after a migration | Either way | Watch which program ids appear in the message |
| Program was upgraded | Either way | Consumption on landed transactions shifts as a step, not a drift |
| Deeper cross-program invocation | Up | Simulation logs show the invoke depth |
| More accounts written in one transaction | Up | Account list length in the built message |
The step-change signature in the fourth row is the useful one. Consumption that jumps at a moment and then stays flat means something upstream changed, and hunting for a market explanation will waste an afternoon. Consumption that drifts gradually is usually route composition moving with liquidity.
Choosing a margin
The margin is a bet between two costs. Too small and you pay for reverts; too large and you pay for units you never use and become slightly harder to schedule. Both are quantifiable, which makes the choice arguable rather than arbitrary.
Here is illustrative arithmetic. Suppose a transaction shape consumes around 90,000 units, and the unit price in force is 10,000 micro-lamports. A limit of 120,000 units costs a priority fee of 1,200 lamports; a limit of 200,000 costs 2,000. The difference is 800 lamports per landed transaction, which across 1,000 transactions is 800,000 lamports, or 0.0008 SOL.
Now the other side. If the tighter limit causes even 20 of those 1,000 transactions to revert, each pays its base fee of 5,000 lamports plus its priority fee of 1,200, which is 124,000 lamports of pure waste, and 20 intents produced nothing. The margin that avoids that is cheap by comparison, and this is the arithmetic that argues against shaving the limit to the last thousand units.
Both figures above are illustrative arithmetic built from the documented base fee and fee formula. The consumption number is a stand-in. The point is the ratio between the two costs, not the values, and that ratio is what makes a generous margin the defensible default.
A sizing sequence
Six steps, each with a cost named, produce a limit that can be defended in a review.
- Build the transaction without a compute budget instruction. Costs nothing and gives a baseline that the runtime will accept.
- Simulate and record units consumed. Costs one call per build. Do it against realistic state, not against a stale fork.
- Collect the same figure from landed transactions of that shape. Costs a field in the attempt log and gives the only measurement taken under real conditions.
- Set the limit above the high end of the observed range. Costs a few hundred lamports at typical prices and removes the revert class entirely.
- Re-measure when the route, venue or program changes. Costs vigilance. The alternative is discovering a shift through reverts.
- Alarm on consumed units approaching the limit. Costs a threshold check and turns a future outage into a scheduled adjustment.
The sixth step is the one that pays for the other five. Consumption approaching the limit is a leading indicator, and it is available in every landed transaction at no additional cost. A sender that watches it will never be surprised by a revert wave; one that does not will meet it during the busiest hour, because that is when routes get longer.
The trade-offs, stated
Every recommendation here has a price, and collecting them in one place makes the whole policy visible rather than scattered across sections.
- Setting an explicit limit costs a simulation call per build and returns lower fees and better schedulability. It is the one nearly free improvement in this entry.
- A generous margin costs lamports on every landed transaction and removes a failure class that costs lamports and produces nothing.
- Splitting a transaction that approaches the per-transaction ceiling costs atomicity, which may be unacceptable depending on what the instructions do together.
- Reducing concurrency to avoid per-account write ceilings costs throughput and is often cheaper than the fee escalation it replaces.
- Re-measuring after every route change costs operational attention and is the only defence against silent consumption drift.
- Alarming on consumption near the limit costs one threshold and converts an incident into a maintenance task.
None of this is specific to any particular sender. Whether the transactions are built by fifty lines of your own code or by a hosted SOL volume bot, the runtime applies the same meter and charges for the same request. The only variable is whether the thing building your transactions has bothered to measure, and whether it will tell you what it requested when you ask.
That last question is a reasonable test of any tooling. A report that includes requested units and consumed units per transaction lets you check the margin policy yourself. A report that includes neither is asking you to take the most controllable cost in the run on trust.
Questions this entry keeps getting
What is a compute unit on Solana?
It is the runtime's unit of metered work. Every operation a program performs during execution costs units, and a transaction is allowed a budget expressed in them. When execution exhausts the budget the transaction is reverted, having landed and paid its fee, which is why the limit is a correctness setting and not only a cost setting.
What is the default compute unit limit?
Without an explicit compute budget instruction the runtime applies a default allowance per instruction and caps the whole transaction at 1,400,000 units. The default is generous for a simple transfer and often wasteful for a swap, because the requested figure is what a priority fee is multiplied by, not the figure actually consumed.
How do I find out how many units my transaction needs?
Simulate it. The simulation response reports units consumed for that run, and a landed transaction reports the same figure after the fact. Those two sources together give a distribution rather than a single number, and the limit should be set against the distribution with a margin you can name.
Does requesting a high limit slow my transaction down?
It does not slow execution, but it makes the transaction larger from a scheduler's point of view, because the requested units are what has to be reserved against a block's capacity. A transaction asking for the maximum is harder to fit into a nearly full block than an equivalent one asking for what it needs.
What happens if I set the limit too low?
Execution stops when the budget is exhausted and the transaction reverts. It landed, so the base fee and the full priority fee are charged, and nothing was accomplished. This failure is worse than over-requesting because it costs money and produces no position, which is why the margin above measured consumption exists.
Are block-level compute limits something I can tune around?
Not directly. A block has a total compute capacity and a lower ceiling on units that may be spent writing any single account, so a heavily traded account can reach its ceiling while the block still has room. The only responses available to a sender are spreading writes across accounts, reducing concurrency, or waiting.
Filed under Latency. Arithmetic on this page is either a documented protocol constant or an illustrative example built from numbers you supply. If a figure here is wrong or has moved, send the desk a correction and the entry gets amended rather than quietly rewritten.