Compute Unit Budgeting for Solana HFT Programs
Every microsecond of compute you waste is a slot you lose. This guide breaks down how to request, measure, and trim compute units in Rust programs so your on-chain logic fits inside the 1.4 M CU limit without triggering ComputeBudgetExceeded errors.
Solana's runtime charges compute units (CUs) for every operation your program executes — arithmetic, account loads, cross-program invocations, syscalls. The per-transaction budget caps at 1,400,000 CU, and if you cross it mid-instruction the runtime rolls back the entire transaction with ComputeBudgetExceeded. For an HFT bot wiring into TierZero's execution layer, this isn't theoretical: a bloated arbitrage instruction that consumes 900 k CU on a quiet devnet can blow past the cap the moment mainnet adds a DEX CPI or an extra account lookup.
How the Compute Budget Instruction Works
Before your program logic runs, you prepend a ComputeBudgetInstruction to the transaction. There are two levers:
set_compute_unit_limit(units: u32)— reserves up tounitsCU for the transaction. Validators use this to schedule and prioritize work.set_compute_unit_price(micro_lamports: u64)— sets your priority fee per CU consumed. Higher price means better placement in the leader's queue.
Both must appear before any other instruction. The runtime enforces the limit strictly: if set_compute_unit_limit is absent, the default is 200,000 CU per instruction (capped at 1.4 M total). For a multi-instruction transaction doing a flash-arb across two AMMs, the default is almost never enough.
A typical budget preamble in Rust client code looks like:
use solana_sdk::compute_budget::ComputeBudgetInstruction;
let cu_limit_ix = ComputeBudgetInstruction::set_compute_unit_limit(600_000);
let cu_price_ix = ComputeBudgetInstruction::set_compute_unit_price(50_000); // micro-lamports per CU
Set the limit to what you actually need plus a small buffer (10–15%), not the theoretical maximum. Validators rank transactions partly on fee-per-CU; padding to 1.4 M when you burn 400 k makes your economics worse and your queue position worse.
Measuring Real CU Consumption
The only reliable source of truth is simulateTransaction with replaceRecentBlockhash: true and commitment: "processed". The response includes unitsConsumed in the metadata. Run this against mainnet-beta with a recent blockhash — devnet CU profiles differ because account state differs.
For on-chain profiling during development, the sol_log_compute_units() syscall emits the remaining CU count to program logs at any point you call it:
use solana_program::log::sol_log_compute_units;
pub fn process_instruction(/* ... */) -> ProgramResult {
sol_log_compute_units(); // baseline remaining
// ... your logic ...
sol_log_compute_units(); // remaining after core logic
Ok(())
}
Diff the two values to isolate exactly how many CUs your hot path costs. Do this at every major block — account deserialization, math, CPI calls — before you optimize blindly. CPI calls are the usual culprit: a single CPI into a DEX like Orca or Raydium costs roughly 1,000–2,000 CU overhead just for the dispatch, on top of whatever the callee burns.
Where CUs Actually Go in an Arb Instruction
For a two-hop swap (Token A → B → C) through two AMMs via CPI, a realistic breakdown on mainnet looks like this:
| Operation | Approximate CU cost |
|---|---|
| Account deserialization (8 accounts) | 8,000–16,000 |
| AMM math (curve calculation, both hops) | 10,000–25,000 |
| CPI dispatch overhead (×2) | 4,000 |
| Each AMM's internal logic | 40,000–120,000 each |
| Token transfers (SPL token CPI ×4) | 20,000–30,000 |
| Total | ~150,000–320,000 |
The AMM's internal logic dominates, and you cannot shrink it — it runs in their program, not yours. What you can control is your wrapper: deserialization strategy, math shortcuts, and minimizing account loads.
Rust-Level Optimizations That Actually Move the Needle
Avoid try_from_slice on large account data. borsh::try_from_slice deserializes the entire buffer. If you only need two fields from a 500-byte account struct, use a zero-copy approach with bytemuck or manually parse the byte offsets. This regularly saves 5,000–15,000 CU per account.
Use integer math over floating-point. The Solana BPF VM has no native FPU; floats are emulated and expensive. Fixed-point arithmetic with Q64.64 or Q32.32 is both faster and deterministic.
Eliminate heap allocations in hot paths. Vec::new(), String::from(), and similar calls in BPF cost CUs for the allocator. Use stack-allocated arrays or reuse pre-allocated buffers passed through account data when possible.
Batch account loads with ALTs (Address Lookup Tables). Each additional account in a v0 transaction costs 25 CU to load regardless of whether you access it. Auditing your account list and removing unused accounts is free CU savings.
Setting the Priority Fee Correctly
Priority fee strategy is inseparable from CU budgeting. Your effective fee is price_micro_lamports × units_consumed / 1_000_000 lamports. If you over-request CUs, you either pay more than necessary or you set a lower unit price to hit the same absolute fee — both outcomes hurt.
The current competitive range for Solana mainnet-beta during active market conditions sits between 10,000 and 500,000 micro-lamports per CU depending on block congestion. Track recent priority fees with getRecentPrioritizationFees and adjust dynamically rather than hardcoding. A static 50 k micro-lamport price that worked last month will get you dropped from blocks during a high-volatility session.
For latency-sensitive positions, some teams run a fee oracle that samples the 75th-percentile fee from the last 20 slots and applies a 1.2× multiplier. Crude, but it keeps you in the block without overreacting to momentary spikes.
Testing Before You Deploy
Simulation catches budget overruns; it does not catch logic errors that surface only at full load. Before promoting an instruction to mainnet:
- Simulate against a mainnet-beta snapshot with realistic account state — use
solana-ledger-toolor a paid RPC withsimulateTransaction. - Add a 20% CU buffer over the simulated
unitsConsumedin your limit instruction. - Log CU checkpoints in your Rust program during a single testnet run with real AMM state loaded, not mocked.
- Monitor
ComputeBudgetExceededcounts in your RPC error logs for the first 48 hours on mainnet.
Simulate obsessively, buffer conservatively, and tighten the limit once you have a week of mainnet data showing stable consumption.
If you want an on-chain program reviewed for CU efficiency or a full HFT pipeline built and operated for you, reach out to the team.
Need a bot like this built?
We design, build and run trading bots on Solana, Hyperliquid and Polymarket.
Start a projectMore from the blog
Public vs Paid RPC: How to Run a Fair Benchmark
A fair comparison of public and paid RPC endpoints must account for quotas, workload, freshness and support guarantees.
Read articleRPC Benchmark Percentiles Explained: p50, p95 and p99
Why averages hide the latency spikes that break trading bots, wallets and production blockchain applications.
Read articleHyperliquid REST vs WebSocket Benchmark: What to Measure
A benchmark plan for Hyperliquid market data that separates request latency from streaming freshness and recovery.
Read article