Engine Room

Compute Budget and Priority: Two Levers, One Fee

A compute unit limit and a compute unit price are different decisions with different consequences. Multiplying them gives you a priority fee, and setting one without measuring the other is how a run spends its budget on the fee line.

The Engine Room Desk 2087 words 10 min read Updated 13 August 2026

Component sheet

Component class
Builder sub-stage
Inputs
Measured compute consumption, a target inclusion behaviour
Outputs
Two compute budget instructions inside the message
Hardest boundary
Fee policy versus cost per successful swap

Solana charges a base fee per signature and an optional priority fee equal to the compute unit limit you request multiplied by the compute unit price you set. Both values are explicit instructions inside the transaction, both are part of the signed message, and the priority fee is charged on the units you requested rather than the units you used.

That last detail decides most of the cost behaviour of an engine. A builder that requests a comfortable limit and a competitive price is paying the competitive price across the comfort margin, several hundred times per run. Measuring consumption once and setting the limit accordingly is usually the largest single cost improvement available, and it takes an afternoon.

Two instructions, two different decisions

The compute budget program exposes the two settings as separate instructions, and keeping them separate in your head is worth the effort because they answer different questions.

SettingQuestion it answersToo lowToo high
Compute unit limitHow much compute does this transaction need?Overrun failure, base fee still chargedPriority fee multiplied across unused units
Compute unit priceHow much am I willing to pay per unit?Poor inclusion when blocks are fullOverpaying on every landed transaction

A transaction that sets neither gets a default allowance per instruction, which is fine for simple transfers and frequently wrong for a swap. A transaction that sets only the price is paying that price across whatever default limit applies. A transaction that sets only the limit has told the network how much room it needs and nothing about how much it values inclusion.

Set both, explicitly, in every transaction an engine builds. The cost of adding two instructions is a handful of bytes in a packet budget you should already be measuring, and the benefit is that fee behaviour becomes a policy you wrote rather than a default you inherited.

How the fee is actually computed

The arithmetic is public and simple, and doing it by hand once removes a lot of superstition. The unit price is denominated in micro-lamports per compute unit, and one lamport is a million micro-lamports. So the priority fee in lamports is the limit multiplied by the price, divided by one million.

priority_lamports = cu_limit * cu_price_micro / 1_000_000
total_lamports    = base_fee_per_signature * signatures + priority_lamports

example:
  cu_limit  = 200_000
  cu_price  = 10_000 micro-lamports
  priority  = 200_000 * 10_000 / 1_000_000 = 2_000 lamports
  base      = 5_000 lamports for one signature
  total     = 7_000 lamports = 0.000007 SOL

Two observations follow immediately. The base fee dominates at low priority settings, which means at quiet times the difference between a careful and a careless fee configuration is small. And the priority component scales linearly with the limit, which means the cost of a generous limit is exactly proportional to how generous it is.

Both of those flip during congestion. When unit prices across the network rise, the priority component becomes the dominant term, and a limit set at twice what you need becomes twice the dominant term. Fee discipline matters least when nothing is happening and most when everything is.

Setting the unit limit from evidence

The procedure is short and there is no substitute for running it. Simulate the exact transaction you intend to send, read the compute units consumed from the response, and set the limit to that figure plus a margin. The margin exists because consumption is not perfectly constant: an account that has to be created, a pool whose internal state has grown, or a route with one extra hop will all consume more than the sample you measured.

  1. Build the transaction exactly as production would, including the account creation instruction if the executor might not have the token account yet. Simulating the optimistic case and running the pessimistic one is a classic way to set a limit that fails intermittently.
  2. Simulate and record the units consumed. Do this for each distinct route shape, not once for the engine, because a two-hop route and a direct swap are different transactions.
  3. Add a margin. Something in the range of ten to twenty per cent covers ordinary variation. A margin of a hundred per cent is not caution, it is a doubling of the priority component.
  4. Re-measure when anything changes: a program upgrade at the venue, a new route shape, or a change to the instruction list such as adding a close instruction.
  5. Record the measured value with the run configuration so that a later cost analysis can distinguish a fee policy change from a consumption change.

The margin is a genuine trade-off rather than a formality. Too small and a proportion of transactions fail with an overrun, and an overrun still costs the base fee, so the failure is not free. Too large and every landed transaction overpays. The right size depends on how variable your measured consumption actually is, which is why step two says record the figure rather than eyeball it.

Setting the unit price without guessing

The price is harder because it is competitive rather than intrinsic. What you are really choosing is a position in the queue for block space, and the right position depends on what everyone else is bidding, which changes minute to minute.

There are three defensible approaches and one indefensible one. You can sample recent prioritisation fees from an endpoint that exposes them and set your price relative to a percentile of that distribution. You can set a fixed price derived from your own tolerance and accept worse inclusion during busy windows. Or you can start low and escalate on retry, which we come back to below. The indefensible approach is copying a number from a forum post, because it encodes somebody else's congestion conditions from an unknown date.

Whichever you choose, express it as a policy with a ceiling. A policy without a ceiling is how an engine ends a congestion event having paid several times its expected total, and the operator only discovers it in the reconciliation. This is one of the largest components of what any tool actually costs to run, and it is the reason a discussion of Solana volume bot cost that ignores the fee line is describing a subscription rather than an operating cost.

A worked cost example

The following is illustrative arithmetic with inputs you would supply from your own measurements. It exists to show which term dominates, not to predict what a run will cost.

Take a run of 400 swaps. Suppose measurement shows the swap consumes 90,000 compute units, and you set the limit at 105,000 with a margin. Suppose the unit price you settle on is 20,000 micro-lamports. The priority component is 105,000 multiplied by 20,000 divided by a million, which is 2,100 lamports. Add the base fee of 5,000 lamports for the single signature and each landed transaction costs 7,100 lamports.

Four hundred landed transactions at 7,100 lamports is 2,840,000 lamports, or roughly 0.00284 SOL in fees for the whole run. Now repeat the calculation with a lazily-set limit of 300,000 units. The priority component becomes 6,000 lamports, the per-transaction total becomes 11,000, and the run costs 4,400,000 lamports. The route, the outcome and the value moved are all identical; the difference is entirely the reservation.

Add attempts to see the second-order effect. Expired attempts cost nothing on chain because they never executed, but overruns do cost the base fee. If five per cent of transactions overrun because the limit was set too tightly, that is twenty transactions paying 5,000 lamports for nothing, which is 100,000 lamports of pure waste plus twenty swaps that did not happen. That is why the margin exists and why it is measured rather than guessed.

Trade-off: inclusion versus cost per successful swap

Raising the unit price improves your odds of landing during contention, and it raises the cost of every transaction that lands, including the ones that would have landed anyway. For a latency-sensitive single transaction that is obviously worth it. For a run of several hundred paced swaps it is a direct multiplier on the metric that actually matters.

The honest framing is that priority fees buy variance reduction, not throughput. If your landing rate is already high at a modest price, paying more buys you very little and costs you linearly.

Fee policy across a run

A fee policy is three numbers and a rule: a baseline unit price, a ceiling, an escalation step, and a statement of when escalation applies. Writing it down turns a set of scattered constants into something an operator can reason about and a post-mortem can evaluate.

Baseline applies to first attempts. Escalation applies only to transactions being rebuilt after expiry, never to plain resends, because a resend by definition uses the same signed bytes and therefore the same fee. That distinction confuses people: you cannot raise the fee on a transaction you are resending. Raising the fee requires building a new transaction, which requires the old one to be provably dead.

The ceiling is what turns the policy from a heuristic into a control. Without it, an escalation loop during a bad window will find the fee level at which anything lands, and that level can be a long way above what the run's economics justify. With it, the engine stops escalating, reports that it stopped, and lets the operator decide whether the run is still worth continuing at all.

Escalation and why it must be bounded

attempt 1: price = baseline
resend   : identical bytes, identical price, until blockhash expiry

on expiry with no landing:
  price = min(price * escalation_factor, ceiling)
  rebuild with fresh blockhash, same intent
  record: expired_at, old_price, new_price, attempt_number

stop when:
  price == ceiling and the rebuild also expired
  -> mark the intent failed, quarantine nothing, report to operator

Notice that the escalation is attached to the intent rather than to the transaction, and that each escalation produces a new signed artefact with its own signature and its own deadline. This is exactly the point where an engine can accidentally duplicate: if the old transaction was not provably expired when the new one was built, both may land.

The defence is to derive the rebuild trigger from the blockhash deadline rather than from a timer of your own choosing. Once the referenced blockhash is outside the accepted window, the old transaction cannot be included, and building a replacement is safe. A rebuild triggered by impatience is a rebuild that can double.

Four expensive anti-patterns

  • Copying a unit price from an unrelated tool. Snipers and volume engines have opposite economics: one sends a handful of transactions where landing is everything, the other sends hundreds where cost per landed swap is everything.
  • Setting a generous limit as insurance. The limit is what you are charged on, so insurance here has a linear premium paid on every success.
  • Escalating on a timer rather than on expiry. This produces duplicate transactions during exactly the windows where you can least afford them.
  • Leaving the fee out of the run's cost metric. If the ledger records value moved but not lamports spent, the engine cannot report cost per successful swap, which is the only fee metric that means anything.

The fourth is the one that hides the other three. An engine that reports swaps and volume but not spend will look identical whether its fee configuration is careful or careless, and the difference only appears when somebody compares the treasury balance against the plan. Any automated Solana volume bot worth evaluating should be able to show spend per landed transaction alongside the activity it produced, because those two numbers together are the only honest description of a run.

Fee configuration checklist

  • Are both compute budget instructions present in every transaction the engine builds?
  • Was the unit limit derived from a simulation of the exact instruction list, including the account creation case?
  • Is the margin above measured consumption stated as a percentage and justified by observed variance?
  • Does the unit price come from a sampled distribution or a stated tolerance, rather than from a copied constant?
  • Is there a ceiling on the unit price, and does hitting it stop the run rather than silently continuing?
  • Does escalation trigger on blockhash expiry rather than on elapsed time?
  • Are the limit, price and resulting fee recorded per transaction in the ledger?
  • Can the engine report cost per successful swap without a manual calculation?

The last item is the acceptance test for this entire note. If cost per successful swap is a number the engine produces on its own, then every setting above is being recorded, and the fee policy can be evaluated rather than argued about. Read the observability note for how that metric is assembled, and the pipeline note for where these instructions sit in the message.

Questions this note gets asked

What is the difference between the compute unit limit and the compute unit price?

The limit is how much compute you reserve for the transaction, measured in compute units. The price is how many micro-lamports you will pay per unit of that reservation. The limit affects whether your transaction fits into a block and whether it fails with an overrun; the price affects how attractive it is to include relative to everything else waiting.

Is the priority fee charged on units consumed or units requested?

On the requested limit. That is the single most important fact in this note, because it means an over-generous limit is not free insurance, it is a multiplier on every landed transaction. Requesting 400,000 units for a swap that consumes 90,000 means paying the unit price across more than four times the compute you used.

What happens if the limit is too low?

The transaction fails with a compute overrun and the base fee is still charged, because the transaction was processed even though it did not succeed. That makes an under-set limit expensive in a different way: you pay to fail, and the failure looks like an application error rather than a configuration one.

Does a higher priority fee guarantee inclusion?

No. It improves your position relative to other transactions competing for the same block space, which is a probabilistic improvement rather than a guarantee. Any engine or tool presenting a fee setting as a landing guarantee is describing something the protocol does not provide.

Should every transaction in a run use the same fee?

Not necessarily, but the variation should follow a stated policy rather than drift. A defensible pattern is a baseline for ordinary releases and a bounded escalation for retries of a transaction that has not landed. What is not defensible is a per-transaction fee derived from a number nobody recorded, because the run cost then cannot be explained afterwards.

How do I measure actual compute consumption?

Simulate the transaction. The simulation response reports units consumed for the exact instruction list you assembled, against current state. Do that once per route shape and again whenever the route or the program changes, and set the limit from the observed figure plus a margin rather than from a default.

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.