Solana Priority Fees: Compute Unit Math for MEV Transactions
Setting compute unit limits and micro-lamport fee prices incorrectly is the single biggest reason searcher transactions fail to land. This tutorial walks through the exact calculation — simulate compute units, set a dynamic fee based on recent percentile data, and cap total lamport spend without sacrificing inclusion probability.
Getting Solana priority fees wrong is the single most common reason MEV transactions fail to land — not simulation errors, not routing bugs. Searchers overpay by 10x on quiet slots and underpay by 5x when a block is contested, and the loss compounds across thousands of attempts. This is the exact math we use in production for Solana MEV and arbitrage bots.
What You Are Actually Paying For
Every Solana transaction consumes compute units (CUs) — a measure of on-chain CPU and memory work. The base fee is a flat 5000 lamports per signature regardless of CUs. The priority fee is separate: it is denominated in micro-lamports per compute unit, so total lamport cost is:
total_lamports = (signatures × 5000) + ceil(requested_CU_limit × micro_lamports_per_CU / 1_000_000)
Two instructions control this:
ComputeBudgetProgram.setComputeUnitLimit(units)— the CU budget the validator allocates to your transactionComputeBudgetProgram.setComputeUnitPrice(microLamports)— the per-CU bid price
The critical mistake is conflating these. Setting a high price does not compensate for a misset limit. If you request 200,000 CUs but only consume 40,000, validators schedule you as if you need 200,000 — hurting your placement. If you request 30,000 and the transaction actually needs 35,000, it reverts at the compute boundary mid-execution.
Simulate First, Then Set the Limit
Never hardcode compute unit limits. Simulate the transaction before submission and add a buffer:
const sim = await connection.simulateTransaction(tx, {
sigVerify: false,
replaceRecentBlockhash: true,
});
if (sim.value.err) throw new Error(`Simulation failed: ${JSON.stringify(sim.value.err)}`);
const consumed = sim.value.unitsConsumed ?? 200_000;
const limit = Math.ceil(consumed * 1.15); // 15% headroom
const cuLimitIx = ComputeBudgetProgram.setComputeUnitLimit({ units: limit });
The 15% buffer is not arbitrary — a live swap through a pool with changed reserves can consume slightly more CUs than the same swap against the simulated state. For complex routes through three or more hops, consider 20%. For simple SOL transfers or trivial program calls, 10% is sufficient.
One detail that bites people: simulateTransaction with replaceRecentBlockhash: true means the simulation does not require a fresh blockhash, but it also means the simulated state may lag by one or two slots. For MEV transactions where the exact pool reserves matter, simulate against a confirmed slot, not the latest.
Pricing the Fee Dynamically
This is where most bots get it wrong. A static 10,000 micro-lamports per CU fails on congested slots and wastes money on quiet ones.
The right approach: pull the recent fee distribution from getRecentPrioritizationFees, target a percentile that matches your inclusion requirements, and apply a multiplier when you detect a contested slot.
const recentFees = await connection.getRecentPrioritizationFees({
lockedWritableAccounts: criticalAccounts, // the accounts your tx writes to
});
const fees = recentFees
.map(f => f.prioritizationFee)
.filter(f => f > 0)
.sort((a, b) => a - b);
const p75 = fees[Math.floor(fees.length * 0.75)];
const base = Math.max(p75, MIN_MICRO_LAMPORTS);
const price = Math.min(base * urgencyMultiplier, MAX_MICRO_LAMPORTS);
Key decisions:
lockedWritableAccounts— pass the specific accounts your transaction writes. Fees vary dramatically by account contention. A fee estimate for a hot Raydium pool account is 3–10x higher than a generic estimate.- Percentile target — use p75 for normal arb, p90 for time-sensitive backruns, p95+ for Jito-routed bundles where you are competing for bundle slot priority.
MIN_MICRO_LAMPORTS— set a floor (e.g. 1,000) so you are never zero on fees. A 0-fee transaction lands only when slots have headroom, which is not the regime you are operating in.MAX_MICRO_LAMPORTS— set a ceiling to prevent runaway spend during anomalous spikes.
Bounding Total Lamport Spend
Knowing the CU limit and the micro-lamport price, you can compute exact maximum spend before signing:
const priorityFeeLamports = Math.ceil((limit * price) / 1_000_000);
const signatureFees = tx.signatures.length * 5000;
const totalWorstCase = priorityFeeLamports + signatureFees;
if (totalWorstCase > MAX_ALLOWED_LAMPORTS) {
// Either lower the price, abort, or use a cheaper route
throw new Error(`Fee budget exceeded: ${totalWorstCase} lamports`);
}
For a typical single-hop arb — one signer, 80,000 CUs at p75 of 50,000 micro-lamports — that is:
priority = ceil(80_000 × 50_000 / 1_000_000) = ceil(4_000_000 / 1_000_000) = 4,000 lamports
total = 4,000 + 5,000 = 9,000 lamports ≈ $0.0014 at $150/SOL
A multi-hop transaction consuming 350,000 CUs at p90 of 200,000 micro-lamports during a contested block:
priority = ceil(350_000 × 200_000 / 1_000_000) = 70,000 lamports ≈ $0.011
The numbers are small individually, but at thousands of submissions per hour the fee regime is a real P&L line. We have seen bots hemorrhage 5–10 SOL per day from misconfigured CU limits and static pricing — the equivalent of getting sandwiched on every trade.
Jito Bundles: Priority Fees vs. Tip Accounts
If you are submitting through Jito, the economics split into two separate components, and conflating them is a classic error:
- Transaction priority fee — still applies, still set via
ComputeBudgetProgram.setComputeUnitPrice. Affects how the Jito block engine orders transactions within the bundle. - Jito tip — a separate SOL transfer to one of the eight Jito tip accounts. This is the bid for bundle inclusion in the leader's block. It does not replace the priority fee.
Both matter. A bundle with a zero priority fee but a large tip may still land, but your transactions inside the bundle will be poorly ordered relative to each other if they compete for the same accounts. A bundle with a high priority fee but an undersized tip gets dropped at the block engine level. The strategy we run in the Solana MEV & Arbitrage Bot calibrates both dynamically: tip scales with estimated profit, priority fee scales with account contention.
Tip amounts to target in practice: 0.001–0.005 SOL covers most non-competitive bundles; 0.01+ SOL is needed when you are racing other searchers on the same opportunity. Never calculate a tip as a fixed percentage of profit without a floor — zero-profit-but-still-valid arbitrage triangles (e.g. dust cycles) will get submitted with zero tips and never land.
What the Numbers Look Like in a Real Slot
To build intuition: run getRecentPrioritizationFees over the last 150 slots on a hot Raydium SOL/USDC account and you will typically see:
- p25: 0–1,000 micro-lamports (quiet slots, off-peak)
- p50: 5,000–15,000 micro-lamports
- p75: 30,000–80,000 micro-lamports
- p90: 100,000–500,000 micro-lamports
- p99: 1,000,000+ micro-lamports (contested events, airdrops, launches)
The distribution is fat-tailed and non-stationary. A bot targeting p75 at calm hours needs to detect when it is suddenly operating in a p99 environment — otherwise it stops landing transactions exactly when the opportunity value is highest. The urgency multiplier in the pricing code above is your lever here: index it to something observable (slot leader, recent skip rate, or just a moving average of recent accepted fees for your account set).
If you are building a Solana searcher and want the fee calibration wired into a production-grade system from day one, reach out — this is exactly the type of infrastructure we ship.
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