Showing a user what a transaction will cost seems like it should be one call. It is three components, and only one of them is fixed.
★An estimate is a number you display. The number you send with has to be computed at send time, and the two are not the same.★
The Three Components
base fee ★5,000 lamports per signature — fixed★
priority fee ★compute unit price × compute unit limit★
rent ★only if you create accounts — and it is not a fee★
The base fee is the only predictable one. It is per signature, so a single-signer transaction costs 5,000 lamports regardless of what it does.
The priority fee is a product, not a value. People quote the price and forget it is multiplied by the requested limit — which means an inflated limit inflates the fee proportionally.
Rent is not a fee at all. It is locked and returned when the account closes, so a cost estimate that lumps it in with fees misrepresents what the user is actually spending.
What getFeeForMessage Covers
const message = new TransactionMessage({
payerKey: payer.publicKey,
recentBlockhash: blockhash,
instructions,
}).compileToV0Message();
const { value: fee } = await connection.getFeeForMessage(message);
★This returns the base fee plus the priority fee implied by any compute budget instructions already in the message.★
Two consequences that catch people:
It does not add a priority fee for you. If you have not included a setComputeUnitPrice instruction, the result is just the base fee — which looks reassuringly cheap and is not what you will pay once you add one.
It does not include rent. Account creation costs are invisible to it, so a swap that creates a token account will cost meaningfully more than this call reports.
If your estimates are right and transactions still miss, a free BoltTx key is one line to test the submission path.
Estimating the Priority Fee
The input has to be the accounts you will contend for, not the network at large:
const fees = await connection.getRecentPrioritizationFees({
lockedWritableAccounts: writableAccounts, // ★specific, not global★
});
const sorted = fees.map((f) => f.prioritizationFee).sort((a, b) => a - b);
const median = sorted[Math.floor(sorted.length / 2)] ?? 0;
const p75 = sorted[Math.floor(sorted.length * 0.75)] ?? median;
★Fee pressure is per-account.★ A quiet market needs almost nothing while a contested pool in a volatility event is a different situation entirely — and a network-wide average describes neither.
The limit half comes from simulation:
const sim = await connection.simulateTransaction(tx, {
replaceRecentBlockhash: true, sigVerify: false,
});
const limit = Math.ceil((sim.value.unitsConsumed ?? 200_000) * 1.2);
const priorityFee = Math.ceil((limit * microLamportsPerCu) / 1_000_000);
★Leaving the limit at its default means paying against a figure well above real usage.★ Setting it from unitsConsumed is the rare change that lowers cost without lowering competitiveness.
Estimate for Display, Compute for Sending
This is the distinction that matters operationally:
// ★Display: a range, computed once, shown to a human.★
const estimate = {
low: baseFee + feeAt(median),
high: baseFee + feeAt(p95) + rentIfCreating,
};
// ★Sending: computed fresh, at send time, every time.★
const sendFee = clamp(feeAt(currentMedian) * multiplier, FLOOR, CEILING);
★A quoted estimate is stale the moment you show it.★ Fees move per slot, so a user who takes ten seconds to confirm is confirming against a number that no longer describes the market.
Show a range rather than a point. A single number invites the complaint that the actual cost differed; a range sets an expectation that survives normal movement.
The Failure This Prevents
Under-reserving is the practical consequence of a bad estimate:
const required = amount
+ baseFee
+ worstCasePriorityFee // ★not the median★
+ rentIfCreatingAccounts
+ rentExemptMinimum; // ★must remain in the account★
★Reserving the median fee means failing during spikes★ — which is exactly when the transaction mattered enough to be contested. A bot that budgets from typical conditions runs out at the worst moment.
What Users Should Be Told
| Component | Show it as |
|---|---|
| Base fee | ★Fixed, negligible★ |
| Priority fee | ★A range, varies with congestion★ |
| Rent | ★Deposit, recoverable★ |
| Slippage | ★Not a fee, but a real cost★ |
★Rent framed as a fee makes your product look expensive for money the user gets back.★ Labelling it a refundable deposit is both more accurate and less alarming.
Slippage belongs in a cost discussion even though it is not a fee — for a wide-tolerance trade it can exceed every fee combined, and a cost estimate that omits it is describing the smaller half.
What Landing Looks Like
Real transactions through our delivery nodes: median confirmation 336ms — under one slot.
★Fees buy position, not certainty.★ An accurate estimate tells the user what a successful attempt costs; it says nothing about how many attempts a wide landing distribution will require.
Where BoltTx Fits
We handle submission. Fee policy stays entirely in your code — we never modify transaction contents, which includes never adjusting your compute budget instructions.
Submissions route through our own delivery nodes in four regions with stake-weighted routing and no public mempool exposure, so a transaction is not observable in transit before it lands.
You sign locally. We never hold funds and never sign. ★The tip travels inside the transaction and is paid on chain from your own wallet, so it belongs in the estimate you show★ — and it reverts with the transaction if it fails, because that is how Solana handles atomic transactions. There is no monthly fee, and you pay only on transactions that reach the chain.
const connection = new Connection("https://la.bolttx.io/?api-key=YOUR_KEY");
FAQ
How do I estimate a Solana transaction fee?
getFeeForMessage returns the base fee plus any priority fee implied by compute budget instructions already in the message. Add rent separately if the transaction creates accounts.
Does getFeeForMessage include the priority fee?
Only if your message already contains a setComputeUnitPrice instruction. Without one it returns just the base fee, which looks cheap and is not what you will pay.
How much is the base fee on Solana? 5,000 lamports per signature. A single-signer transaction pays that regardless of complexity, which makes it the one component you can predict exactly.
How is the priority fee calculated? Compute unit price multiplied by the compute unit limit you requested. Because it is a product, an inflated limit raises the fee proportionally even if the transaction uses far less.
Why does my actual fee differ from the estimate? Fees move per slot, so an estimate shown to a user is stale by the time they confirm. Compute the fee fresh at send time and show a range rather than a point.
Should rent be included in a fee estimate? Show it separately and label it a refundable deposit. It is locked rather than spent and returns when the account closes, so calling it a fee overstates what the user is losing.
How do I estimate fees for a swap? Base fee, plus a priority fee derived from recent activity on the pool accounts, plus rent if a token account must be created. The third is often the largest for a new token.
What accounts should I query for recent fees? The writable accounts your transaction touches. Fee pressure is per-account, so a network-wide average describes neither a quiet market nor a contested one.
How do I find the right compute unit limit?
Simulate the transaction and read unitsConsumed, then add a margin. Leaving the default means paying priority fees against a figure well above real usage.
Should I reserve the median fee or the worst case? The worst case. Reserving the median means failing during spikes, which is exactly when the transaction was contested enough to matter.
Is slippage a fee? No, but it is a real cost and belongs in any honest cost discussion. On a wide-tolerance trade it can exceed every fee combined.
How do I show fees to users without alarming them? A range for the priority fee, the base fee noted as fixed and negligible, and rent labelled as a recoverable deposit rather than a charge.
Can I estimate fees without building the transaction?
Only roughly. getFeeForMessage needs a compiled message, and the compute limit needs simulation, so an accurate estimate requires the instructions you plan to send.
Why is my fee estimate too low during congestion? Because it was computed from a calm-period sample. Recompute at send time from current data on the specific accounts, and clamp the result between a floor and a ceiling.
Does a failed transaction still cost the estimated fee? If it landed and reverted, the base fee and priority fee were paid. If it never landed, no fee was charged, which is why expiry and revert must be tracked separately.
What should the fee ceiling be? Derived from the value of the transaction rather than a round number. Hitting it should raise an alert so you know conditions are extreme instead of silently paying through a spike.