getRecentPrioritizationFees returns a recent-slot window of per-slot samples, each a { slot, prioritizationFee } pair where prioritizationFee is the fee a paying transaction set for that slot in micro-lamports per compute unit, not a total fee. A naive chain-wide call can return an all-zero array or a sample dominated by non-paying transactions, so a robust estimator must explicitly handle no samples and all zeros rather than blindly averaging. Scoping the call to your program's writable hot account yields a more relevant signal than a chain-wide sample, and selecting a percentile (p50 for cost sensitivity, p75–p90 for faster inclusion) from sorted values is more robust than the mean. Combine the chosen micro-lamports-per-CU with the transaction's compute-unit limit to get the actual extra lamports, and refetch on a short interval because samples shift every slot. This article builds a local estimator in Node.js, shows how to set SetComputeUnitPrice on a VersionedTransaction, and provides a reproducible results table and honest limitations.
What getRecentPrioritizationFees Actually Returns
The Solana JSON-RPC method getRecentPrioritizationFees returns a recent-slot window of per-slot samples. Each sample is a pair of { slot, prioritizationFee }, where prioritizationFee is the fee a paying transaction set for that slot in micro-lamports per compute unit — not a total fee in lamports. The Solana getRecentPrioritizationFees reference documents the parameters and units, and the Solana priority fee guide explains how micro-lamports per compute unit relate to SetComputeUnitPrice.
The method accepts an optional addresses parameter. When supplied, the node scopes the sample to slots whose transactions touched those writable accounts. This matters because a chain-wide sample mixes every program's fee behavior, while scoping to your program's hot account gives a signal that reflects the contention your transaction will actually face. The Solana priority fee estimation and compute unit price article frames the compute-unit-price concept; this article focuses on building the estimator itself.
Because the response is a window of recent slots, it is inherently a snapshot. Samples shift every slot, so any cached value ages quickly. Treat the array as a short-lived observation, not a stable configuration value.
- Each sample: { slot, prioritizationFee }.
- prioritizationFee unit: micro-lamports per compute unit.
- Optional addresses parameter scopes the sample to slots touching those writable accounts.
- The window is recent and shifts every slot.
Why a Naive Chain-Wide Call Is Unreliable
A naive call with no addresses parameter can return an all-zero array or a sample dominated by non-paying transactions. This is a well-known trap: many transactions set no priority fee, so the chain-wide distribution is heavily weighted toward zero. Averaging that distribution produces a number that is technically derived from the data but practically useless for inclusion under contention.
The second failure mode is an empty or all-zero result. If every sample is zero, the mean is zero, and a naive estimator will set a zero priority fee. A robust estimator must handle 'no samples' and 'all zeros' explicitly — for example, by falling back to a configured floor rather than emitting zero.
The third failure mode is scoping mismatch. A chain-wide sample may look healthy while your specific writable account is cold, or vice versa. Scoping to the account your transaction will write gives a more relevant signal, but it can also return fewer samples, which makes explicit empty-array handling even more important.
- All-zero arrays: common when few transactions pay a priority fee.
- Non-paying dominance: the mean is dragged toward zero.
- Scoping mismatch: chain-wide signal may not reflect your account's contention.
- Explicit handling of no samples and all zeros is mandatory.
Scoping the Sample to Your Writable Hot Account
The addresses parameter accepts a list of writable accounts. The node returns samples only for slots whose transactions touched those accounts. For a program with a single hot state account, passing that account focuses the sample on the contention that matters. For a program with multiple hot accounts, you can pass several, but be aware that the returned window may be sparser.
Scoping is not free: a narrow scope can return fewer samples, and in quiet periods it can return none. That is why the estimator must treat an empty scoped result as a signal to fall back — either to a wider scope or to a configured floor — rather than as a zero fee.
When you operate against a managed endpoint such as OnFinality's Solana network, the node's view of recent slots is what the method reflects. Different endpoints can return different samples, so the estimator should be validated against the endpoint you actually send through.
- Pass the writable account your transaction will touch.
- Narrow scope can return fewer or zero samples.
- Fall back to a wider scope or a floor when scoped samples are empty.
- Validate against the endpoint you send through.
Choosing a Percentile Instead of the Mean
Once you have a non-empty, non-all-zero sample, sort the prioritizationFee values and select a percentile. The mean is sensitive to outliers and to the zero-heavy tail; a percentile is a more stable summary of the distribution you care about. p50 is appropriate when cost sensitivity dominates and you accept slower inclusion. p75 to p90 is appropriate when faster inclusion matters more than cost.
After selecting a percentile, apply a floor and a cap. The floor prevents a zero or near-zero result from producing a transaction that will not be included under contention. The cap prevents a single outlier sample from producing an absurd fee. Both bounds should be configuration values you can tune per program, not hard-coded constants buried in the estimator.
The chosen value is in micro-lamports per compute unit. To convert it to the extra lamports the user pays, multiply by the transaction's compute-unit limit and divide by one million. That extra amount is added to the base fee, and it is the number that should appear in any user-facing estimate.
- Sort prioritizationFee values, then select p50, p75, or p90.
- Apply a floor to avoid zero-fee transactions.
- Apply a cap to bound outlier influence.
- Extra lamports = micro-lamports-per-CU × compute-unit limit ÷ 1,000,000.
Recency, Cadence, and Staleness Detection
Samples shift every slot, so caching a fee for minutes is a correctness bug, not an optimization. Refetch on a short interval or immediately before each send. If you must cache, cache for a duration measured in slots, not minutes, and invalidate aggressively.
Detect a stale sample window by reading the current slot and comparing it to the maximum slot in the returned samples. If the gap exceeds your tolerance, treat the window as stale and refetch. The Solana commitment levels and transaction confirmation article explains how commitment affects what the node considers recent.
Cadence interacts with rate limits. If you refetch before every send, your request volume scales with your transaction volume. The Solana RPC timeouts and retries and Solana RPC latency guide articles cover the operational side; the estimator should treat a failed refetch as a reason to use the last known good value plus a floor, not to send a zero fee.
- Refetch on a short interval or before each send.
- Compare current slot to max sample slot to detect staleness.
- On refetch failure, use last known good value plus floor.
- Request volume scales with transaction volume.
Runnable Node.js Estimator with and without Address Scope
The following example calls getRecentPrioritizationFees twice — once chain-wide and once scoped to a writable account — filters out reference zero samples, computes p50, p75, and p90, and prints the results. It uses the standard JSON-RPC 2.0 request shape described in the JSON-RPC 2.0 specification and the Ethereum JSON-RPC conventions for method/params structure where applicable.
Replace the endpoint and the scoped address with your own. The example deliberately does not assert any latency or throughput number; it only demonstrates the request and the percentile computation.
const ENDPOINT = process.env.SOLANA_RPC_URL || 'https://your-endpoint.example';
const SCOPED_ADDRESS = process.env.SCOPED_ADDRESS || 'YourWritableAccountPubkey';
async function rpc(method, params) {
const res = await fetch(ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params })
});
const json = await res.json();
if (json.error) throw new Error(JSON.stringify(json.error));
return json.result;
}
function percentile(sorted, p) {
if (sorted.length === 0) return null;
const idx = Math.min(sorted.length - 1, Math.floor((p / 100) * sorted.length));
return sorted[idx];
}
function summarize(samples) {
const fees = samples
.map(s => s.prioritizationFee)
.filter(f => typeof f === 'number' && f > 0)
.sort((a, b) => a - b);
if (fees.length === 0) return { count: 0, p50: null, p75: null, p90: null };
return {
count: fees.length,
p50: percentile(fees, 50),
p75: percentile(fees, 75),
p90: percentile(fees, 90)
};
}
(async () => {
const chainWide = await rpc('getRecentPrioritizationFees', []);
const scoped = await rpc('getRecentPrioritizationFees', [[SCOPED_ADDRESS]]);
console.log('chain-wide', summarize(chainWide));
console.log('scoped', summarize(scoped));
})();Setting SetComputeUnitPrice on a VersionedTransaction
The chosen micro-lamports-per-CU value is applied to the transaction via the SetComputeUnitPrice instruction, which is part of the compute budget program. The Solana priority fee guide documents this instruction. The example below builds a VersionedTransaction, sets the compute unit limit and the compute unit price, and prints the resulting micro-lamports per CU.
Note that the compute unit limit and the compute unit price are separate settings. The limit bounds how much compute the transaction may consume; the price sets how much you pay per unit. The extra lamports the user pays is the product of the two, divided by one million.
const { Connection, PublicKey, TransactionMessage, VersionedTransaction, ComputeBudgetProgram } = require('@solana/web3.js');
const ENDPOINT = process.env.SOLANA_RPC_URL || 'https://your-endpoint.example';
const connection = new Connection(ENDPOINT, 'confirmed');
async function buildTx(payer, instructions, microLamportsPerCU, computeUnitLimit) {
const { blockhash } = await connection.getLatestBlockhash('confirmed');
const message = new TransactionMessage({
payerKey: payer,
recentBlockhash: blockhash,
instructions: [
ComputeBudgetProgram.setComputeUnitLimit({ units: computeUnitLimit }),
ComputeBudgetProgram.setComputeUnitPrice({ microLamports: microLamportsPerCU }),
...instructions
]
}).compileToV0Message();
const tx = new VersionedTransaction(message);
console.log('micro-lamports per CU:', microLamportsPerCU);
console.log('compute unit limit:', computeUnitLimit);
console.log('extra lamports:', Math.floor((microLamportsPerCU * computeUnitLimit) / 1_000_000));
return tx;
}
module.exports = { buildTx };Reproducible Results Table for Your Endpoint
Because the method reflects the local node's view of recent slots, different endpoints can return different samples. To compare endpoints or to tune your estimator, fill the table below with your own measurements. Do not treat any published number as a substitute for measuring against the endpoint you actually send through.
Run the estimator at a fixed cadence, record the sample count and the percentiles, and note the current slot and the maximum sample slot so you can compute staleness. Repeat across several intervals to see variance.
- Endpoint: the RPC URL you called.
- Scope: chain-wide or the writable account used.
- Sample count: number of non-zero prioritizationFee values.
- p50 / p75 / p90: micro-lamports per CU.
- Current slot and max sample slot: for staleness.
- Chosen fee and floor/cap: what you actually set.
Limitations and Tradeoffs
Values are micro-lamports per compute unit and must not be confused with total lamports. A fee between p50 and p90 still does not guarantee inclusion under congestion; it is a heuristic, not a contract. The method reflects the local node's view of recent slots, so different endpoints can return different samples, and a scoped call can return fewer samples than a chain-wide call.
Preflight via simulateTransaction is a separate signal from the fee market. A successful simulation says the transaction is likely to execute; it says nothing about whether the fee is competitive. Conversely, a competitive fee does not guarantee the transaction will simulate successfully. Treat them as independent inputs.
Finally, the estimator is only as good as its cadence. A stale window produces a stale fee, and a zero-fee fallback produces a transaction that may never land. The floor and cap are the safety rails that keep the estimator from emitting pathological values.
- Micro-lamports per CU ≠ total lamports.
- p50–p90 does not guarantee inclusion.
- Endpoint view differs; scoped samples can be sparse.
- Preflight and fee market are independent signals.
- Stale windows and zero fallbacks are the main failure modes.
Troubleshooting Common Estimator Failures
When the estimator returns zero, first check whether the raw response was an empty array or an all-zero array. If it was empty, your scope may be too narrow or the endpoint may be behind. If it was all zeros, the sample is dominated by non-paying transactions; widen the scope or raise the floor.
When the estimator returns a value that seems too high, check for a single outlier sample and confirm the cap is applied. When the transaction is not included despite a competitive fee, check the compute unit limit — an underestimated limit can cause the transaction to fail before the fee matters. The Solana API guide (RPC Assistant) covers endpoint-level diagnostics, and the OnFinality Learn hub collects related operational guides.
When results differ between endpoints, that is expected. Record both in the results table and decide which endpoint you will send through. If you need a managed endpoint with consistent behavior, review RPC pricing and the API service options.
- Zero result: distinguish empty array from all-zero array.
- Too high: check for outliers and confirm the cap.
- Not included: check compute unit limit and preflight.
- Endpoint differences: record both and choose your send path.
Next Steps for Production Deployment
Move the estimator behind a small service that refetches on a short interval, applies the floor and cap, and exposes the chosen micro-lamports-per-CU to your transaction builder. Log the sample count, percentiles, and staleness so you can diagnose regressions. Validate against the endpoint you send through, and revisit the floor and cap as your program's contention changes.
For related operational topics, see the Solana priority fee estimation and compute unit price article for the conceptual framing, and the Solana RPC timeouts and retries and Solana RPC latency guide articles for the transport side. When you are ready to compare endpoints, the OnFinality Solana network page and RPC pricing are the starting points.
- Wrap the estimator in a small refetching service.
- Log sample count, percentiles, and staleness.
- Validate against your send endpoint.
- Revisit floor and cap as contention changes.