All articles
Smart Contracts·April 13, 2026·6 min read

Solana Compute Budget Optimization for Trading Bot Programs

A concrete guide to setting ComputeBudgetProgram instructions, profiling CU consumption with solana-measure, and restructuring account iteration to keep complex arbitrage instructions inside the 1.4 M CU ceiling. Includes before/after benchmarks from a real Hyperliquid bridging program.

Solana compute budget optimization is the difference between an arbitrage transaction landing cleanly in-block and getting dropped at the scheduler — and once you are running a production MEV or cross-venue arb bot, that distinction is measured in real money. Every Solana transaction is capped at 1,400,000 compute units (CU) by default, but the ceiling is meaningless if you do not know how close you are to it, or if you are burning CUs on logic that has no business being on-chain.

How the ComputeBudgetProgram Actually Works

The Solana runtime allocates compute before it executes a transaction. You request budget and price via two separate ComputeBudgetProgram instructions, and they must be prepended to the instruction list — not appended.

let set_limit = ComputeBudgetInstruction::set_compute_unit_limit(400_000);
let set_price = ComputeBudgetInstruction::set_compute_unit_price(50_000); // micro-lamports per CU

let tx = Transaction::new_signed_with_payer(
    &[set_limit, set_price, your_arb_instruction],
    Some(&payer.pubkey()),
    &[&payer],
    recent_blockhash,
);

set_compute_unit_price is your priority fee knob. The effective lamport cost is ceil(requested_CU × unit_price / 1_000_000). A bot requesting 400,000 CU at 50,000 micro-lamports pays 20,000 lamports in priority fees — roughly $0.003 at current SOL prices, which is irrelevant against arb profits but becomes material if your CU estimate is wildly conservative and you are requesting three times what you use.

Two common mistakes:

  • Requesting the full 1.4 M CU as a lazy default. You pay for what you request, and over-requesting makes your transaction look expensive to validators without giving you anything in return.
  • Not setting a limit at all. Without an explicit set_compute_unit_limit, the runtime applies the 200,000 CU default per instruction, which will halt complex multi-DEX routing mid-execution.

Profiling with solana-measure and Program Logs

Before you can optimize, you need numbers. The cleanest way to instrument a Solana program is the solana_program::log::sol_log_compute_units() call, which emits the remaining CU balance into the transaction log at any point in your program.

use solana_program::log::sol_log_compute_units;

pub fn process_arb(ctx: Context<ArbAccounts>, params: ArbParams) -> Result<()> {
    sol_log_compute_units(); // baseline — how many CUs did account deserialization cost?

    let pool_a = ctx.accounts.pool_a.load()?;
    sol_log_compute_units(); // after first load

    // ... routing logic ...

    sol_log_compute_units(); // before swap CPI
    invoke_cpi(&ctx)?;
    sol_log_compute_units(); // after swap CPI — the expensive part

    Ok(())
}

Pull the logs from a devnet run with solana logs <program_id> or parse them from SimulateTransaction. The difference between consecutive sol_log_compute_units readings gives you the exact CU cost of each block of logic. For off-chain profiling of compute-heavy Rust code, solana-measure (solana_measure::measure::Measure) wraps wall-clock and CU timing cleanly, but the on-chain log approach is ground truth.

Before/After: Hyperliquid Bridging Program

One program we shipped bridges positions from a Solana vault to Hyperliquid L1 via a cross-chain message instruction. The initial implementation consumed 1,210,000 CU per call, leaving only 190,000 as headroom — a single reordering by the scheduler could push it over the edge. Here is where the budget was going:

Stage Before (CU) After (CU)
Account deserialization (8 accounts) 47,000 22,000
Price oracle reads (3 feeds) 310,000 140,000
Routing logic + fee calc 88,000 88,000
Cross-program invocations (2 CPIs) 540,000 480,000
Cross-chain message serialization 225,000 91,000
Total 1,210,000 821,000

The three changes that drove the reduction:

1. Replace Account<'info, T> with AccountLoader<'info, T> for large structs. Anchor's standard Account wrapper deserializes the entire account into a local struct on access. For accounts with large data fields (order books, state machines), switching to AccountLoader and calling .load() / .load_mut() defers deserialization and halves the CU cost — 47,000 → 22,000 in this case.

2. Cache oracle reads; do not re-read within the same instruction. Each AccountInfo read re-enters the account table. Three feeds being read twice each cost us 170,000 extra CU. Store the price in a u64 after the first read and reference the local variable.

3. Pre-compute serialization off-chain where possible. The cross-chain message was being serialized inside the program from a complex struct. Moving the serialization to the client and passing a pre-built Vec<u8> byte slice cut the on-chain cost by 134,000 CU. The program validates a hash of the payload instead of rebuilding it.

Account Iteration and the Hidden CU Tax

Multi-pool arbitrage programs often iterate over a dynamic set of accounts — scanning six or eight pools to find the best route. The naïve pattern passes a remaining_accounts slice and iterates it with a standard loop. This works, but every AccountInfo clone inside the loop re-charges the account-loading CU overhead.

// expensive: re-borrows the account data on every iteration
for account in ctx.remaining_accounts.iter() {
    let pool = Pool::try_deserialize(&mut &account.data.borrow()[..])?;
    if pool.liquidity > best_liquidity {
        best_pool = Some(account.key());
        best_liquidity = pool.liquidity;
    }
}

Better pattern: deserialize once into a fixed-size stack array (if the pool count is bounded), then operate on the local copies.

const MAX_POOLS: usize = 8;
let mut pools: [Option<Pool>; MAX_POOLS] = [const { None }; MAX_POOLS];

for (i, account) in ctx.remaining_accounts.iter().take(MAX_POOLS).enumerate() {
    pools[i] = Some(Pool::try_deserialize(&mut &account.data.borrow()[..])?);
}

// all comparisons now hit stack memory, not account storage

On a six-pool scan, this alone dropped CU consumption from 310,000 to 170,000 in a routing function we were optimizing for a Solana MEV and arb program.

Setting the Right Limit in Production

Once you have profiled numbers, do not set the limit to the exact measured value. The runtime's CPI cost varies with network state, and Anchor adds small overhead on each version bump. Use your measured peak plus 15–20% headroom, then monitor sol_log_compute_units at the end of successful transactions in production to watch for drift.

A sensible production pattern for a client-side bot:

const CU_LIMIT = 450_000; // measured peak 380k + 18% buffer
const CU_PRICE = await getDynamicPriorityFee(); // from Jito or priority fee API

const budgetIxs = [
    ComputeBudgetProgram.setComputeUnitLimit({ units: CU_LIMIT }),
    ComputeBudgetProgram.setComputeUnitPrice({ microLamports: CU_PRICE }),
];

const tx = new Transaction().add(...budgetIxs, ...arbIxs);

Dynamic priority fees — pulled from the Jito tip floor or a recent-fee percentile API — are worth the extra RPC call. A static fee calibrated on a quiet afternoon will under-bid during high-congestion blocks, and your arb will miss inclusion. Budget time and priority fees are the two levers you actually control; treat them as first-class parameters, not afterthoughts.

What Does Not Belong On-Chain

The deepest CU savings come from not putting logic on-chain in the first place. Route-finding, optimal sizing calculations, fee modeling, slippage estimates — these belong in the off-chain bot. The program should receive a pre-computed route_plan and execute it atomically, validating only what it must (prices are within acceptable bounds, balances are sufficient). Every conditional branch you eliminate from the program is CUs you never spend.

This is the architecture we default to on every Solana smart contract build at TierZero: thin, verifiable on-chain execution; heavy logic in the off-chain engine where it costs nothing.


If you are building a Solana trading program and hitting CU limits or leaving priority fees unoptimized, reach out — this is exactly the kind of problem we fix as part of a full bot build.

Need a bot like this built?

We design, build and run trading bots on Solana, Hyperliquid and Polymarket.

Start a project
#Solana#Smart Contracts#Rust#Anchor#MEV#Arbitrage#Performance