Solana Compute Budget: How to Right-Size CU Limits in Bot Txns
Setting compute unit limits too high wastes priority fee spend while setting them too low causes silent transaction failures mid-execution — and most bot developers never simulate CU consumption before going live. This post shows how to use simulateTransaction to profile actual consumption, automate limit padding by instruction type, and monitor compute efficiency across a fleet.
Getting the Solana compute budget wrong in bot transactions is one of those bugs that hides until it costs real money. Setting your CU limit too high inflates priority fees because the network charges price × limit, not price × consumed — so every wasted unit is direct overhead. Setting it too low triggers a ComputeBudgetExceeded error that kills the transaction mid-instruction, often after partial state changes have already been written (most instructions are atomic, but complex multi-program flows can leave audit logs or intermediate account states in place). Right-sizing compute unit limits in bot transactions is not optional on a production system; it is table stakes.
Why the Default 200,000 CU Cap Is Almost Always Wrong
Every transaction that omits SetComputeUnitLimit gets the network default of 200,000 compute units. For a simple SPL transfer that consumes around 3,000 CU, you are overpaying your priority fee by a factor of 66x. For a multi-hop swap through two AMMs that consumes 185,000 CU, you are one complex path variation away from a silent revert.
The real problem is instruction composition. A sniper bot transaction might bundle: a compute budget instruction, a Jito tip transfer, an associated-token-account creation, and a swap through Raydium or Pump.fun. Each program invocation carries its own CU overhead. CPI calls from your program into system programs cost roughly 1,000 CU apiece. Account lookups add up. Anchor's discriminator checks, account validation macros, and event-logging all consume budget you may not have accounted for when you hardcoded 200,000.
The gap between "it works in staging" and "it reverts on mainnet" is usually a hot path — a token account that already exists in staging gets created on mainnet, adding 8,000–12,000 CU to the total.
Profiling Actual CU Consumption with simulateTransaction
The correct workflow is to simulate before you set limits, not set limits before you simulate. The simulateTransaction RPC method runs your transaction against the current ledger state without broadcasting it, and the response includes unitsConsumed:
const sim = await connection.simulateTransaction(tx, {
sigVerify: false,
commitment: "processed",
accounts: { encoding: "base64", addresses: [] },
});
if (sim.value.err) {
throw new Error(`Simulation failed: ${JSON.stringify(sim.value.err)}`);
}
const consumed = sim.value.unitsConsumed ?? 0;
console.log(`Simulation: ${consumed} CU consumed`);
Run this against a transaction that has no SetComputeUnitLimit instruction (or one set to the maximum, 1,400,000). The simulator will report actual usage under current ledger conditions. That number is your baseline.
One important caveat: simulation runs against a snapshot, not the live cluster. Parallel transactions touching the same accounts can change compute paths — an account that exists during simulation may not exist at landing time, triggering an on-chain creation branch that costs more CU than your simulation reported. This is why you always add a safety buffer.
Automating CU Limit Padding by Instruction Type
A flat percentage buffer works as a starting point but is too blunt for a fleet of bots running different instruction sets. The right approach is to bucket by instruction complexity and apply tiered padding:
const CU_PADDING: Record<string, number> = {
simple_transfer: 1.10, // 10% — low variance
swap_single_hop: 1.15, // 15% — minor path variance
swap_multi_hop: 1.25, // 25% — multiple AMMs, ATA creation risk
bundle_complex: 1.30, // 30% — multi-program, conditional branches
};
function buildWithBudget(
instructions: TransactionInstruction[],
simulatedCU: number,
txType: keyof typeof CU_PADDING,
cuPriceMicroLamports: number
): Transaction {
const limit = Math.ceil(simulatedCU * CU_PADDING[txType]);
return new Transaction().add(
ComputeBudgetProgram.setComputeUnitLimit({ units: limit }),
ComputeBudgetProgram.setComputeUnitPrice({ microLamports: cuPriceMicroLamports }),
...instructions
);
}
For sniper bots hitting Pump.fun, I use 1.25 as the default because new pools trigger associated-token-account creation on the first buy — an extra 8,000–10,000 CU the simulator will not always catch if it's running against a state where that ATA already exists. For a MEV arbitrage transaction running atomic cross-DEX routes, 1.30 is conservative but appropriate because the CPI tree is deeper and path execution depends on pool state that can shift between simulation and landing.
The SetComputeUnitLimit instruction itself costs 150 CU and SetComputeUnitPrice costs another 150 CU. Small, but worth knowing — they factor into the simulation total when you include them in the simulated transaction.
The Priority Fee Impact of Getting CU Limits Right
This is where the math becomes concrete. Suppose your priority fee is 100,000 micro-lamports per compute unit (a reasonable competitive price for a moderately busy slot). At a 200,000 CU limit, your priority fee is:
200,000 × 100,000 / 1,000,000 = 20,000 lamports ≈ 0.00002 SOL
If your actual consumption is 35,000 CU and you right-size to 43,000 CU (35,000 × 1.25), your priority fee drops to:
43,000 × 100,000 / 1,000,000 = 4,300 lamports ≈ 0.0000043 SOL
That is a 78% reduction in fee spend per transaction. Run a copytrading bot executing 500 transactions per day and the daily saving is roughly 7,850,000 lamports — a non-trivial operational cost at scale, particularly during high-throughput campaigns. This is the arithmetic that makes simulation non-negotiable in a serious Solana trading bot operation.
For a deeper treatment of how CU pricing interacts with retry logic under congestion, see our earlier post on Solana transaction retry logic and compute unit fee strategy.
Monitoring Compute Efficiency Across a Fleet
Single-bot CU tuning is straightforward. Fleet-level monitoring is what separates production systems from demo code. The key metrics to log per transaction:
cu_simulated: what simulation reportedcu_limit_set: the padded limit you sentcu_consumed: what the confirmed transaction actually used (available ingetTransactionwithmaxSupportedTransactionVersion: 0)cu_efficiency:cu_consumed / cu_limit_set— your utilization ratiocu_padding_hit: boolean,cu_consumed > cu_simulated— simulation underestimated
Track these in a time-series store (Postgres with TimescaleDB, InfluxDB, or just structured logs shipped to Grafana). Alert on two conditions: cu_efficiency dropping below 0.5 (you are significantly over-budgeting) or cu_padding_hit rate exceeding 5% over a 1-hour window (your buffer tiers need upward revision).
A useful operational pattern for high-frequency bots: maintain a rolling percentile window of cu_consumed per transaction type over the last 1,000 executions. At each build step, set your limit to p95_consumed × 1.15. This auto-adjusts to network state changes — new program versions, account state shifts — without requiring manual re-profiling. It does lag by a few hundred transactions, so bootstrap with conservative static values and let the percentile window converge.
Path Variance: When Simulation Lies
One edge case that catches teams on multi-AMM routes: some programs branch on whether a user has an existing fee-discount account, referral PDA, or loyalty token. The simulation runs against the current account state; your production wallet's state at landing time may differ. The clearest example is OpenBook and similar order-book programs where a user's open-order account presence affects the CPI path taken.
The fix is to explicitly set the CU limit on every retry with fresh simulation results — not to cache the simulated value across attempts. If your retry loop reuses a stale limit from 800 ms ago, you are exposed to exactly this drift. Simulating on each build is cheap; a failed transaction because you reused a stale CU estimate is expensive.
We handle this in fleet code by making simulation a non-optional step in the transaction builder, not a call site decision. The builder refuses to produce a transaction without a fresh simulation result unless the caller explicitly passes { skipSimulation: true } with a documented override reason.
If you want a Solana bot built with this level of compute-budget discipline baked in from day one — not bolted on after the first missed trade — talk to TierZero. We design, build and operate these systems in production.
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