Every Solana transaction pays a small, fixed base fee. When the network is busy, that isn't enough to get scheduled quickly, so you add a priority fee. The priority fee isn't a flat tip: it's a price per compute unit multiplied by a compute unit limit you declare. Getting both numbers right is what makes transactions land cheaply, and getting the limit wrong is one of the most common ways to overpay.
What a transaction actually pays
A Solana fee has two parts:
| Part | How it's calculated | Notes |
|---|---|---|
| Base fee | 5,000 lamports per signature | Fixed. Doesn't change with congestion or with how much compute you use. |
| Priority fee | compute unit limit × compute unit price | Optional. The price is in micro-lamports (millionths of a lamport) per CU. |
So the priority fee in lamports is:
priority fee = ceil(computeUnitLimit × computeUnitPrice / 1,000,000)
A transaction requesting 200,000 CUs at 10,000 micro-lamports per CU pays 2,000 lamports of priority fee, on top of 5,000 lamports for one signature. Both parts are charged even if the transaction lands and then fails.
The important detail is that the priority fee is charged on the limit you request, not on the compute you actually use.
Compute units and their limits
A compute unit (CU) measures the work a transaction's instructions do: each BPF instruction executed, syscalls, cryptographic operations, and so on. There are two limits to know:
- Per transaction: 1,400,000 CUs. No single transaction can use more.
- Default when you don't set one: 200,000 CUs per instruction, capped at 1.4M. A transaction with three program instructions gets a default limit of 600,000. Recent validator versions give instructions to built-in programs, such as the System Program, a much smaller default.
If execution exceeds the limit, the transaction fails with "exceeded CUs meter" and you still pay the fee. Blocks also have a total CU budget, and each writable account has its own cap per block, which matters for fee markets below.
The Compute Budget program
You control both numbers with instructions to the Compute Budget program, placed in the same transaction:
ComputeBudgetProgram.setComputeUnitLimit({ units })sets the limit.ComputeBudgetProgram.setComputeUnitPrice({ microLamports })sets the price per CU.
Each may appear at most once per transaction; duplicates make the transaction invalid. By convention they go first, though the runtime reads them wherever they are. If you set a price but no limit, the price applies to the default limit, which is usually far more than you need.
Why over-requesting costs you twice
Many snippets set a limit of 1,400,000 "to be safe". That has two costs:
- You pay for it. At 10,000 micro-lamports per CU, a 1.4M limit costs 14,000 lamports of priority fee even if the transaction uses 40,000 CUs. A 50,000 limit would cost 500.
- It's harder to schedule. When a leader packs a block, it has to reserve your requested CUs against the block's limit and against the per-account limit for every account you write. A transaction claiming 1.4M CUs is harder to fit than one claiming 50,000, especially when the block or a hot account is nearly full.
The fix is to simulate the transaction, read how many CUs it consumed, and request that plus a margin. A 10–20% margin covers normal variation. If a code path depends on on-chain state that can change between simulation and execution (a swap crossing more ticks, a loop over a growing list), leave more.
Choosing a price: local fee markets
Solana's fee market is local. Transactions only compete with other transactions that want to write the same accounts. A popular token launch or a busy AMM pool can be congested while a simple transfer between two quiet wallets needs little or no priority fee at all.
That's why fee estimation takes the accounts you'll write:
connection.getRecentPrioritizationFees({ lockedWritableAccounts })returns, for each of up to roughly the last 150 slots, the minimum priority fee (micro-lamports per CU) paid by a landed transaction that write-locked those accounts. Because it reports the minimum per slot, it is often zero; take a percentile of the non-zero values rather than the average.- RPC provider APIs do the percentile work for you. Helius offers
getPriorityFeeEstimate, and other major providers have their own equivalents. They usually return tiers such as low, medium and high, and are generally more useful than the raw RPC method.
Cap the price you're willing to pay. A fee estimator reacting to a spike can otherwise quote a price you'd never accept.
A complete example with @solana/web3.js
This builds a versioned transaction, estimates a price from the writable accounts, simulates to size the CU limit, then signs with both compute budget instructions:
import {
ComputeBudgetProgram,
Connection,
Keypair,
PublicKey,
TransactionInstruction,
TransactionMessage,
VersionedTransaction,
} from "@solana/web3.js";
const MAX_PRICE_MICRO_LAMPORTS = 500_000; // your ceiling
const MIN_PRICE_MICRO_LAMPORTS = 1_000;
async function estimatePrice(
connection: Connection,
writable: PublicKey[],
): Promise<number> {
const recent = await connection.getRecentPrioritizationFees({
lockedWritableAccounts: writable,
});
const fees = recent
.map((f) => f.prioritizationFee)
.filter((f) => f > 0)
.sort((a, b) => a - b);
if (fees.length === 0) return MIN_PRICE_MICRO_LAMPORTS;
const p75 = fees[Math.floor(fees.length * 0.75)];
return Math.min(Math.max(p75, MIN_PRICE_MICRO_LAMPORTS), MAX_PRICE_MICRO_LAMPORTS);
}
export async function buildPrioritizedTx(
connection: Connection,
payer: Keypair,
instructions: TransactionInstruction[],
): Promise<VersionedTransaction> {
const writable = instructions.flatMap((ix) =>
ix.keys.filter((k) => k.isWritable).map((k) => k.pubkey),
);
const microLamports = await estimatePrice(connection, writable);
const { blockhash } = await connection.getLatestBlockhash("confirmed");
const compile = (units: number) =>
new TransactionMessage({
payerKey: payer.publicKey,
recentBlockhash: blockhash,
instructions: [
ComputeBudgetProgram.setComputeUnitLimit({ units }),
ComputeBudgetProgram.setComputeUnitPrice({ microLamports }),
...instructions,
],
}).compileToV0Message();
// Simulate with the maximum limit so the simulation itself can't run out of CUs.
const sim = await connection.simulateTransaction(
new VersionedTransaction(compile(1_400_000)),
{ 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 units = Math.min(Math.ceil(consumed * 1.15) + 1_000, 1_400_000);
const tx = new VersionedTransaction(compile(units));
tx.sign([payer]);
return tx;
}
The simulation includes both compute budget instructions so its CU count matches the final transaction. Send the result and handle expiry the way you would any other transaction: a priority fee helps a transaction get scheduled, but it doesn't change how blockhashes expire or how you should rebroadcast. The separate guide on "Blockhash not found" and dropped transactions covers the send-and-confirm loop.
The same thing with @solana/kit
With the newer @solana/kit stack, the instructions come from the @solana-program/compute-budget package:
import {
getSetComputeUnitLimitInstruction,
getSetComputeUnitPriceInstruction,
} from "@solana-program/compute-budget";
const budgetInstructions = [
getSetComputeUnitLimitInstruction({ units: 60_000 }),
getSetComputeUnitPriceInstruction({ microLamports: 5_000n }),
];
The same package includes helpers that estimate the limit by simulating the transaction message, so check its current exports before writing your own simulation step.
Jito tips
A large share of Solana stake runs the Jito validator client, which accepts bundles: a group of transactions executed in order, all or nothing, with a tip paid by a SOL transfer to one of Jito's tip accounts. Tips are separate from priority fees and go through Jito's block engine rather than the normal scheduler. They're mainly used by traders and bots that need atomic ordering or MEV protection. For ordinary app transactions, a correctly sized priority fee is usually enough, and a tip only helps when the current leader runs Jito.
Summary
| Question | Answer |
|---|---|
| What's the base fee? | 5,000 lamports per signature, regardless of congestion. |
| How is the priority fee calculated? | CU limit × CU price (micro-lamports) ÷ 1,000,000, in lamports. |
| What limit should I request? | Simulated consumption plus a 10–20% margin, never more than 1.4M. |
| How do I pick the price? | A percentile of recent fees for the accounts you write, from the RPC or a provider API, with a ceiling. |
| Why is my fee high when the network seems quiet? | You're writing to a hot account, or you're paying for a default or oversized CU limit. |
Get the weekly commit
New blockchain deep dives every week.