The formula that determines what you pay
Solana transaction fees have two parts:
- A base fee, currently 5,000 lamports per signature.
- An optional priority fee, set through compute-budget instructions.
The priority-fee formula is:
priority fee (lamports)
= ceil(compute-unit price (micro-lamports) × compute-unit limit / 1,000,000)If a transaction requests a 220,000 compute-unit limit and bids 25,000 micro-lamports per compute unit:
ceil(25,000 × 220,000 / 1,000,000) = 5,500 lamports
one-signature total fee = 5,000 base + 5,500 priority = 10,500 lamportsThe important detail is easy to miss: Solana calculates the priority fee from the requested limit, not the compute units the transaction actually consumes. If the same transaction only uses 140,000 units, it still pays against the 220,000-unit request. It also pays the fee when execution fails.
The official Solana fee documentation defines both the formula and the current base fee. Treat those protocol docs as the source of truth because fee mechanics can change.
The expensive mistake is not only bidding too many micro-lamports. It is multiplying that bid by a compute-unit limit you never needed.
Compute-unit limit and compute-unit price do different jobs
The two compute-budget instructions are independent:
| Control | What it changes | Common failure |
|---|---|---|
| Compute-unit limit | Maximum compute the transaction may consume | Too low causes compute exhaustion; too high pays for unused headroom |
| Compute-unit price | Bid in micro-lamports for each requested compute unit | Too low loses scheduling priority; too high burns margin |
The default limit is 200,000 compute units for each non-builtin instruction, up to a transaction maximum of 1.4 million. Defaults are broad safety rails, not a fee policy. Solana's compute-budget guide recommends simulating the transaction and adding a 10% safety margin to the consumed units.
This is the right sequence:
build → simulate → set CU limit → sample relevant fees → set CU price → sign → send → observeDo not add multiple instructions of the same compute-budget type. Duplicate limit or price instructions
can cause a DuplicateInstruction error.
A production-safe TypeScript pattern
The following example uses @solana/web3.js. It simulates with a generous temporary limit, applies a 10%
margin, and then rebuilds the final transaction with the selected fee.
import {
ComputeBudgetProgram,
Connection,
PublicKey,
TransactionInstruction,
TransactionMessage,
VersionedTransaction,
} from "@solana/web3.js";
const MAX_COMPUTE_UNITS = 1_400_000;
type BuildArgs = {
connection: Connection;
payer: PublicKey;
instructions: TransactionInstruction[];
microLamports: number;
};
export async function buildPriorityTransaction({
connection,
payer,
instructions,
microLamports,
}: BuildArgs) {
const { blockhash } = await connection.getLatestBlockhash("confirmed");
const simulationMessage = new TransactionMessage({
payerKey: payer,
recentBlockhash: blockhash,
instructions: [
ComputeBudgetProgram.setComputeUnitLimit({ units: MAX_COMPUTE_UNITS }),
...instructions,
],
}).compileToV0Message();
const simulation = await connection.simulateTransaction(
new VersionedTransaction(simulationMessage),
{ sigVerify: false },
);
if (simulation.value.err) {
throw new Error(`Simulation failed: ${JSON.stringify(simulation.value.err)}`);
}
const consumed = simulation.value.unitsConsumed ?? 200_000;
const units = Math.min(MAX_COMPUTE_UNITS, Math.ceil(consumed * 1.1));
const finalMessage = new TransactionMessage({
payerKey: payer,
recentBlockhash: blockhash,
instructions: [
ComputeBudgetProgram.setComputeUnitLimit({ units }),
ComputeBudgetProgram.setComputeUnitPrice({ microLamports }),
...instructions,
],
}).compileToV0Message();
return {
transaction: new VersionedTransaction(finalMessage),
units,
estimatedPriorityFeeLamports: Math.ceil((microLamports * units) / 1_000_000),
};
}The returned transaction is unsigned. Sign only after the final message is built. Rebuilding after signing changes the message and invalidates the signature.
The 10% margin is a starting point, not a law. Programs with input-dependent compute may need a wider
buffer. Measure unitsConsumed across real instruction shapes, not one happy-path fixture.
Solana has local fee markets
A cluster-wide median is often the wrong input. Transactions compete with other transactions that lock the same writable accounts. A quiet transfer and a crowded DEX route can face different prices in the same slot.
The native getRecentPrioritizationFees
method accepts up to 128 account addresses and returns recent fees for transactions that would lock all of
those accounts as writable. Validators cache up to 150 blocks of observations.
That gives you a better estimator loop:
- Extract the transaction's writable accounts.
- Request recent prioritization fees for that account set.
- Remove stale or unusable observations.
- Choose a percentile based on urgency.
- Apply a maximum fee cap.
- Reprice from fresh data when the blockhash or opportunity changes.
You can inspect current percentiles with the rpc edge Priority Fee Estimator before implementing the same policy in your sender.
Choose a percentile from the action, not one global preset
One fee preset cannot serve every transaction. The loss from a missed liquidation is not the same as the loss from a delayed housekeeping instruction.
| Action | Sensible starting policy | Why |
|---|---|---|
| Background maintenance | Lower percentile, conservative cap | Delay is cheaper than overpayment |
| User swap | Mid percentile, bounded retries | Balance confirmation time and user cost |
| Time-sensitive rebalance | Upper percentile, strict expiry | The action loses value quickly |
| Liquidation or arbitrage | Opportunity-priced cap | Fee ceiling should follow expected profit and failure cost |
For a trading strategy, define the cap from expected value:
maximum all-in landing cost
< expected gross edge - slippage budget - risk reserveThat converts fee policy from an arbitrary number into a risk rule. If the required bid crosses the cap, skip the trade. Paying to land a negative-expectancy action is not better execution.
Priority fee, Jito tip, SWQoS, and delivery are separate levers
These mechanisms act at different stages:
| Lever | What it buys | What it cannot fix |
|---|---|---|
| Priority fee | Better native scheduling priority | A packet that never reaches the leader |
| Jito tip | Competitiveness in the Block Engine auction | Native scheduling on a non-Jito path |
| SWQoS | Stake-weighted access to leader ingest | Low priority after admission |
| Direct leader delivery | A shorter path to current and upcoming leaders | Invalid execution or an uneconomic bid |
A Jito tip is not a substitute for a priority fee. Jito's low-latency transaction documentation describes the Block Engine path and its auction. Use a tip when that path or bundle behavior is part of your execution. Use the native priority fee to compete in Solana's scheduler.
Likewise, a high priority fee does nothing if the packet is dropped before scheduling. That is why stake-weighted QoS and direct leader paths matter. The complete landing path is delivery, admission, scheduling, and execution.
Price the fee, then shorten the path.
Estimate the current priority fee for your workload, then send through rpc edge's stake-weighted, leader-aware transaction path.
Retries need a budget and an expiry condition
Retries should not mean sending forever. Reuse the same signed transaction only while its blockhash is valid and the underlying action still makes sense. Stop when any of these conditions becomes true:
- The signature reaches the required commitment.
- The blockhash expires.
- The opportunity deadline passes.
- The fee cap or retry budget is exhausted.
- The action is no longer profitable or safe.
If you rebuild with a fresh blockhash, you create a different transaction and signature. Before doing that, check whether the prior transaction landed. Otherwise two valid versions of the same intent may execute.
Our guide to why Solana transactions fail covers stale blockhashes, preflight errors, account contention, and delivery failures beyond fee selection.
Measure fee policy by landing outcome
An estimator is only a hypothesis until you compare it with results. Record at least:
signature
strategy and transaction class
submission timestamp and slot
target leader and delivery path
writable-account set or route identifier
requested CU limit
simulated and actual CU consumption
CU price and estimated priority fee
Jito tip, if any
first observed slot
confirmed or finalized slot
error, expiry, and retry countThen group results by transaction class, region, delivery path, and fee percentile. The useful charts are landing rate and time-to-land against fee paid. Average fee alone tells you nothing about the fills you missed.
Watch the tail. A policy that looks efficient at p50 can still collapse during the congested slots that contain the highest-value opportunities.
Production checklist
Before shipping a priority-fee policy:
- Simulate the final instruction shape.
- Set one compute-unit limit with a measured safety margin.
- Set one compute-unit price from recent, relevant account data.
- Cap fees in lamports and in expected-value terms.
- Keep Jito tips separate in configuration and telemetry.
- Use a fresh blockhash and an explicit expiry condition.
- Send to the leaders that can include the transaction.
- Measure landing rate, not accepted RPC responses.
- Recalibrate by transaction class and congestion regime.
The takeaway
Priority fees are not a magic speed field. They are a bid applied to the compute budget you request. Set the limit from simulation, set the price from the local writable-account market, and cap both against the value of the action.
Then engineer the rest of the path. The fee determines ordering only after a transaction arrives and is admitted. A leader-aware transaction sender, stake-weighted ingest, fresh blockhashes, and bounded retries turn that fee into a landing policy you can measure.