39 min read
Overview
Solana is a high-throughput blockchain, producing blocks roughly every 400ms. This enables the network to achieve some of the highest transactions per second (TPS) of any blockchain today.
In Solana's model, transactions compete for inclusion in blocks. A "failed" transaction is rarely due to network downtime. In fact, Solana has maintained 100% uptime for the past 17 months (as of September 2025). Failures are more commonly caused by:
- The blockhash sent with the transaction having expired.
- The transaction using more compute than allowed in its compute budget.
- A transaction was out-bid by other transactions with a higher priority fee, particularly during times of peak network demand.
In this guide, we'll focus on optimizations to ensure transactions are included in blocks, such as:
- Priority Fees & Compute Unit Optimization - How to use priority fees provided by QuickNode API and estimate your transaction's compute unit usage
- Transaction Blockhash Management - Understanding different precommitment values for fetching your blockhash
- Transaction Retries & Expiration handling - How to retry transaction submission with expiration handling
- Sending Transaction with QuickNode SDK - Learn how to use
SmartTransactions
provided by QuickNode SDK - Using Stake-Weighted Quality of Service (QoS)
- Using Jito Bundles
This guide is available in Solana Web3.js (Legacy v 1.x) and Solana Kit (v 3.x) versions. Select the appropriate tab to view the code snippets and instructions for your preferred library:
- Solana Web3.js (Legacy)
- Solana Kit
The explanations throughout this guide primarily reference the Solana Web3.js (Legacy) code examples. However, the Solana Kit implementations follow the same conceptual patterns and logic—just with updated syntax and method calls.
Let's get started!
Priority Fees
Solana's fee priority system allows you to set an additional fee on top of the base fee for a transaction, which gives your transaction a higher priority in the leader's queue. By bidding more for priority status, your transaction will be more likely to be confirmed quickly by the network. A higher priority fee does not guarantee your transaction's inclusion in the block, but it does give the transaction priority amongst others being processed in the same thread. Most transactions today utilize priority fees, so ignoring them could risk your transaction getting dropped.
We have a dedicated guide to using priority fees on Solana that explains how they work in detail. However, in this guide, we will focus on how you might determine an appropriate priority fee level for your business requirements.
QuickNode has a Priority fee API, which will fetch the recent priority fees paid across the last (up to) 100 blocks for the entire network or a specific program account. The method qn_estimatePriorityFees
returns priority fees in 5% percentiles and convenient ranges (low
, medium
, high
, extreme
, and recommended
). Here is an example of how you can fetch the latest fees in your TypeScript application:
- Solana Web3.js (Legacy)
- Solana Kit
import { Transaction, ComputeBudgetProgram } from "@solana/web3.js";
import { RequestPayload, ResponseData, EstimatePriorityFeesParams } from "./types";
async function fetchEstimatePriorityFees({
last_n_blocks,
account,
api_version,
endpoint
}: EstimatePriorityFeesParams): Promise<ResponseData> {
const params: any = {};
if (last_n_blocks !== undefined) {
params.last_n_blocks = last_n_blocks;
}
if (account !== undefined) {
params.account = account;
}
if (api_version !== undefined) {
params.api_version = api_version;
}
const payload: RequestPayload = {
method: 'qn_estimatePriorityFees',
params,
id: 1,
jsonrpc: '2.0',
};
const response = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data: ResponseData = await response.json();
return data;
}
import {
Rpc,
createDefaultRpcTransport,
createRpc,
RpcTransport,
createJsonRpcApi,
RpcRequest
} from "@solana/kit";
import {
EstimatePriorityFeesResponse,
SolanaKitEstimatePriorityFeesParams
} from "./types";
interface createQuickNodeTransportParams {
endpoint: string;
}
type PriorityFeeApi = {
qn_estimatePriorityFees(params: SolanaKitEstimatePriorityFeesParams): EstimatePriorityFeesResponse;
// Add other methods here
}
function createQuickNodeTransport({ endpoint }: createQuickNodeTransportParams): RpcTransport {
const jsonRpcTransport = createDefaultRpcTransport({ url: endpoint });
return async <TResponse>(...args: Parameters<RpcTransport>): Promise<TResponse> => {
return await jsonRpcTransport(...args);
};
}
export function createPriorityFeeApi(endpoint: string): Rpc<PriorityFeeApi> {
const api = createJsonRpcApi<PriorityFeeApi>({
requestTransformer: (request: RpcRequest<any>) => {
const transformedRequest = {
...request,
params: request.params[0]
};
return transformedRequest;
},
responseTransformer: (response: any) => response.result,
});
const transport = createQuickNodeTransport({
endpoint,
});
return createRpc({ api, transport });
}
async function main() {
const quickNodeRpc = createPriorityFeeApi(''); // 👈 Replace with your QuickNode endpoint
const priorityFees = await quickNodeRpc.qn_estimatePriorityFees({
last_n_blocks: 100,
account: 'JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4',
api_version: 2
}).send();
console.log(priorityFees);
}
main();
You can add the types to your project by references in our examples repository.
A sample response:
{
"jsonrpc": "2.0",
"result": {
"context": {
"slot": 254387670
},
"per_compute_unit": {
"extreme": 1686993,
"high": 260140,
"low": 10714,
"medium": 50000,
"percentiles": {
"5": 100,
"10": 827,
"15": 2000,
// ...
"90": 510498,
"95": 1686993,
"100": 20000000000,
}
},
"per_transaction": {
"extreme": 4290114,
"high": 500000,
"low": 50000,
"medium": 149999,
"percentiles": {
"5": 700,
"10": 2000,
"15": 9868,
// ...
"90": 1250000,
"95": 4290114,
"100": 20000000000,
}
},
"recommended": 400251
},
"id": 1
}
During periods of high network congestion, to increase the likelihood of successful transaction processing, we recommend using the recommended
property of the response. We monitor the network conditions and adjust the fee level accordingly to maximize the likelihood of successful transaction processing.
You can now select a priority fee level that suits your business requirements. Let's now take a look at compute unit optimization, and then we will discuss how to use both of these methods in your transaction assembly.
Compute Unit Optimization
Every transaction on Solana uses compute units (CU) to process. The more complex the transaction, the more compute units it will consume. The network has a limit on the number of compute units that can be processed in a single block. If your transaction exceeds this limit, it will be dropped.
As of September 2025, the following limits and fee structure are in place:
Compute Unit Limits:
- Max Compute per block: 60 million CU
- Max Compute per account per block: 12 million CU
- Max Compute per transaction: 1.4 million CU
- Default compute unit limit per transaction: 200,000 CU
Fee Structure:
- Base transaction fee: 5000 lamports per signature (determinable via
getFeeForMessage
RPC method) - Incremental cost per CU: 0 by default (no prioritization fee unless specified)
Important Notes:
- The default 200,000 CU limit can be increased up to 1.4 million CU by including a
SetComputeUnitLimit
instruction - Without a
SetComputeUnitPrice
instruction, you pay no additional fee per compute unit beyond the base transaction fee - Priority fees are only applied when you explicitly set a compute unit price using
SetComputeUnitPrice
- Solana Web3.js (Legacy)
- Solana Kit
// Example: Setting custom compute limit and price
const transaction = new Transaction()
.add(
// Increase compute limit from default 200,000 to 400,000
ComputeBudgetProgram.setComputeUnitLimit({ units: 400_000 })
)
.add(
// Set priority fee (optional - without this, incremental cost per CU is 0)
ComputeBudgetProgram.setComputeUnitPrice({ microLamports: 10_000 })
)
.add(...yourInstructions);
// Example: Setting custom compute limit and price
import {
pipe,
createTransactionMessage,
appendTransactionMessageInstructions,
} from "@solana/kit";
import {
getSetComputeUnitLimitInstruction,
getSetComputeUnitPriceInstruction,
} from "@solana-program/compute-budget";
// Increase CU limit from default 200,000 to 400,000
const computeUnitLimitInstruction = getSetComputeUnitLimitInstruction({
units: 400_000,
});
// Set priority fee (optional - without this, incremental cost per CU is 0)
const computeUnitPriceInstruction = getSetComputeUnitPriceInstruction({
microLamports: 10_000,
});
// Build transaction message
const transactionMessage = pipe(
createTransactionMessage({ version: 0 }),
(tx) => appendTransactionMessageInstructions([
computeUnitLimitInstruction,
computeUnitPriceInstruction,
...yourInstructions, // Add your other instructions here
], tx)
);
Since there has been no history of charging for compute units, there has been little incentive or need to optimize transactions sent to the network. This means that many applications use the default 200,000 CU per transaction or the maximum (to avoid transaction errors). This is not ideal, especially during times of high network traffic, as it can lead to dropped transactions.
Fortunately, you can simulate your transaction before sending it to the cluster to determine the compute units it consumes. This will allow you to send your transaction to the cluster with the least compute units possible, increasing the likelihood of your transaction being included in a block.
To calculate your transaction's compute units, use the simulateTransaction
RPC method from the Solana Web3.js library. Here is an example of how you can use this method in your TypeScript application:
- Solana Web3.js (Legacy)
- Solana Kit
async getSimulationUnits(
connection: Connection,
instructions: TransactionInstruction[],
payer: PublicKey
): Promise<number | undefined> {
const testInstructions = [
ComputeBudgetProgram.setComputeUnitLimit({ units: 1_400_000 }),
...instructions,
];
const testVersionedTxn = new VersionedTransaction(
new TransactionMessage({
instructions: testInstructions,
payerKey: payer,
recentBlockhash: PublicKey.default.toString(),
}).compileToV0Message()
);
const simulation = await connection.simulateTransaction(testVersionedTxn, {
replaceRecentBlockhash: true,
sigVerify: false,
});
if (simulation.value.err) {
return undefined;
}
return simulation.value.unitsConsumed;
}
// Simulate transaction to get compute units
async function getSimulationUnits(
rpc: Rpc<SolanaRpcApi>,
instructions: Instruction[],
payerSigner: any
): Promise<number | undefined> {
const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();
const testInstructions = [
getSetComputeUnitLimitInstruction({ units: 1_400_000 }),
...instructions,
];
const transactionMessage = pipe(
createTransactionMessage({ version: 0 }),
(tx) => appendTransactionMessageInstructions(testInstructions, tx),
(tx) => setTransactionMessageFeePayerSigner(payerSigner, tx),
(tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx)
);
const transaction = await signTransactionMessageWithSigners(transactionMessage);
const rawTx = getBase64EncodedWireTransaction(transaction);
try {
const simulation = await rpc.simulateTransaction(rawTx, {
commitment: 'confirmed',
encoding: 'base64',
sigVerify: false,
replaceRecentBlockhash: true,
}).send();
if (simulation.value.err != null || simulation.value.unitsConsumed == null) {
console.warn('Simulation failed:', simulation.value.err);
return undefined;
}
return Number(simulation.value.unitsConsumed);
} catch (error) {
console.error('Error during simulation:', error);
return undefined;
}
}
A few things to note about our function:
- Make sure to include a
setComputeUnitLimit
instruction in your test instructions. This is because our transaction will require this instruction to update the compute unit limit. You can use the max value for the simulation since we are only interested in the number of compute units consumed. - If you are using priority fees, make sure to include the priority fee instruction in your
instructions
array. We will do this in our example below.
Optimized Transaction Assembly
Now that we have tools for fetching recent priority fees and calculating the number of compute units our transaction will consume, we can use these tools to create a transaction assembly that will increase the likelihood of our transaction being included in a block.
Here is an example of how you can use these tools in your TypeScript application:
- Solana Web3.js (Legacy)
- Solana Kit
import { Connection, Keypair, Transaction, ComputeBudgetProgram } from "@solana/web3.js";
import { fetchEstimatePriorityFees, getSimulationUnits } from "./helpers"; // Update with your path to our methods
const endpoint = YOUR_QUICKNODE_ENDPOINT; // Replace with your QuickNode endpoint
const keyPair = Keypair.generate();// derive your keypair from your secret key
async function main(){
// 1. Establish a connection to the Solana cluster
const connection = new Connection(endpoint);
// 2. Create your transaction
const transaction = new Transaction();
// ... add instructions to the transaction
// 3. Fetch the recent priority fees
const { result } = await fetchEstimatePriorityFees({ endpoint });
const priorityFee = result.recommended; // Replace with your priority fee level based on your business requirements, e.g. result.per_compute_unit['high']
// 4. Create a PriorityFee instruction and add it to your transaction
const priorityFeeInstruction = ComputeBudgetProgram.setComputeUnitPrice({
microLamports: priorityFee,
});
transaction.add(priorityFeeInstruction);
// 5. Simulate the transaction and add the compute unit limit instruction to your transaction
let [units, recentBlockhash] =
await Promise.all([
getSimulationUnits(
connection,
transaction.instructions,
keyPair.publicKey
),
connection.getLatestBlockhash(),
]);
if (units) {
units = Math.ceil(units * 1.05); // margin of error
transaction.add(ComputeBudgetProgram.setComputeUnitLimit({ units }));
}
// 6. Sign and send your transaction
transaction.feePayer = keyPair.publicKey;
transaction.recentBlockhash = recentBlockhash.blockhash;
transaction.sign(keyPair);
const hash = await connection.sendRawTransaction(
transaction.serialize(),
{ skipPreflight: true, maxRetries: 0 }
);
return hash;
}
import {
Instruction,
Rpc,
SolanaRpcApi,
pipe,
createTransactionMessage,
appendTransactionMessageInstructions,
setTransactionMessageFeePayerSigner,
setTransactionMessageLifetimeUsingBlockhash,
getBase64EncodedWireTransaction,
createSolanaRpc,
lamports,
generateKeyPairSigner,
getAddressFromPublicKey,
signTransactionMessageWithSigners,
createDefaultRpcTransport,
createRpc,
RpcTransport,
createJsonRpcApi,
RpcRequest,
airdropFactory,
createSolanaRpcSubscriptions
} from "@solana/kit";
import {
getSetComputeUnitLimitInstruction,
getSetComputeUnitPriceInstruction,
} from "@solana-program/compute-budget";
import {
getTransferSolInstruction,
} from "@solana-program/system";
import {
EstimatePriorityFeesResponse,
SolanaKitEstimatePriorityFeesParams
} from "./types";
// Types for QuickNode priority fee estimation
type PriorityFeeApi = {
qn_estimatePriorityFees(params: SolanaKitEstimatePriorityFeesParams): EstimatePriorityFeesResponse;
};
// Helper function to create QuickNode RPC transport
function createQuickNodeTransport({ endpoint }: { endpoint: string }): RpcTransport {
const jsonRpcTransport = createDefaultRpcTransport({ url: endpoint });
return async <TResponse>(...args: Parameters<RpcTransport>): Promise<TResponse> => {
return await jsonRpcTransport(...args);
};
}
// Create priority fee API client
function createPriorityFeeApi(endpoint: string): Rpc<PriorityFeeApi> {
const api = createJsonRpcApi<PriorityFeeApi>({
requestTransformer: (request: RpcRequest<any>) => {
const transformedRequest = {
...request,
params: request.params[0]
};
return transformedRequest;
},
responseTransformer: (response: any) => response.result,
});
const transport = createQuickNodeTransport({ endpoint });
return createRpc({ api, transport });
}
// Simulate transaction to get compute units
async function getSimulationUnits(
rpc: Rpc<SolanaRpcApi>,
instructions: Instruction[],
payerSigner: any
): Promise<number | undefined> {
const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();
const testInstructions = [
getSetComputeUnitLimitInstruction({ units: 1_400_000 }),
...instructions,
];
const transactionMessage = pipe(
createTransactionMessage({ version: 0 }),
(tx) => appendTransactionMessageInstructions(testInstructions, tx),
(tx) => setTransactionMessageFeePayerSigner(payerSigner, tx),
(tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx)
);
const transaction = await signTransactionMessageWithSigners(transactionMessage);
const rawTx = getBase64EncodedWireTransaction(transaction);
try {
const simulation = await rpc.simulateTransaction(rawTx, {
commitment: 'confirmed',
encoding: 'base64',
sigVerify: false,
replaceRecentBlockhash: true,
}).send();
if (simulation.value.err != null || simulation.value.unitsConsumed == null) {
console.warn('Simulation failed:', simulation.value.err);
return undefined;
}
return Number(simulation.value.unitsConsumed);
} catch (error) {
console.error('Error during simulation:', error);
return undefined;
}
}
// Main function - Optimized transaction with priority fees and compute units
async function main() {
// Use devnet for testing (has faucet for funding accounts)
const rpc = createSolanaRpc('http://127.0.0.1:8899');
const rpcSubscriptions = createSolanaRpcSubscriptions('ws://127.0.0.1:8900');
const airdrop = airdropFactory({ rpc, rpcSubscriptions });
const QUICKNODE_ENDPOINT = "YOUR_QUICKNODE_ENDPOINT";
console.log('=== GENERATING ACCOUNTS ===');
// Generate sender and receiver accounts
const senderSigner = await generateKeyPairSigner();
const recipientSigner = await generateKeyPairSigner();
const senderAddress = await getAddressFromPublicKey(senderSigner.keyPair.publicKey);
const recipientAddress = await getAddressFromPublicKey(recipientSigner.keyPair.publicKey);
console.log('Sender:', senderAddress);
console.log('Recipient:', recipientAddress);
console.log('\n=== FUNDING SENDER ACCOUNT ===');
await airdrop({
recipientAddress: senderAddress,
lamports: lamports(1_000_000_000n),
commitment: 'confirmed',
});
console.log('\n=== CREATING TRANSFER INSTRUCTION ===');
// Create transfer instruction
const instructions: Instruction[] = [
getTransferSolInstruction({
source: senderSigner,
destination: recipientAddress,
amount: lamports(1_000_000n), // 0.001 SOL
}),
];
console.log('\n=== FETCHING PRIORITY FEES ===');
// Fetch priority fee using QuickNode API
const quickNodeRpc = createPriorityFeeApi(QUICKNODE_ENDPOINT);
const priorityFeesResponse = await quickNodeRpc.qn_estimatePriorityFees({
last_n_blocks: 100,
api_version: 2
}).send();
const priorityFee = priorityFeesResponse.recommended;
console.log('\n=== ADDING PRIORITY FEE INSTRUCTION ===');
// Add priority fee instruction
const priorityFeeInstruction = getSetComputeUnitPriceInstruction({
microLamports: priorityFee,
});
// Add priority fee instruction to the beginning of the instructions array
instructions.unshift(priorityFeeInstruction);
console.log(`Added priority fee instruction with ${priorityFee} microlamports`);
console.log('\n=== SIMULATING FOR COMPUTE UNITS ===');
// Simulate the transaction and add the compute unit limit instruction
const computeUnits = await getSimulationUnits(rpc, instructions, senderSigner);
if (computeUnits) {
console.log(`Transaction would consume: ${computeUnits} compute units`);
const adjustedUnits = Math.ceil(computeUnits * 1.1); // 10% margin
console.log(`Using compute units with 10% margin: ${adjustedUnits}`);
const computeUnitLimitInstruction = getSetComputeUnitLimitInstruction({
units: adjustedUnits
});
instructions.unshift(computeUnitLimitInstruction);
console.log('Added compute unit limit instruction');
} else {
console.log('Could not determine compute units from simulation, using default limit');
const computeUnitLimitInstruction = getSetComputeUnitLimitInstruction({
units: 200_000 // Default fallback
});
instructions.unshift(computeUnitLimitInstruction);
console.log('Added default compute unit limit instruction');
}
console.log('\n=== BUILDING TRANSACTION ===');
// Get latest blockhash
const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();
console.log('Latest blockhash:', latestBlockhash.blockhash);
// Create transaction message
const transactionMessage = pipe(
createTransactionMessage({ version: 0 }),
(tx) => appendTransactionMessageInstructions(instructions, tx),
(tx) => setTransactionMessageFeePayerSigner(senderSigner, tx),
(tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx)
);
console.log('\n=== SIGNING AND SENDING ===');
// Sign transaction
const signedTransaction = await signTransactionMessageWithSigners(transactionMessage);
const serializedTransaction = getBase64EncodedWireTransaction(signedTransaction);
// Send transaction
const signature = await rpc
.sendTransaction(serializedTransaction, {
preflightCommitment: 'confirmed',
encoding: 'base64',
skipPreflight: false,
maxRetries: 0n
})
.send();
console.log('\n✅ TRANSACTION COMPLETED!');
console.log('Signature:', signature);
}
main()
Let's break down the steps in our example:
- We establish a connection to the Solana cluster.
- We create our transaction. This is where you would add your instructions to the transaction.
- We fetch the recent priority fees using our
fetchEstimatePriorityFees
method. - We create a priority fee instruction and add it to our transaction. You can customize your priority fee level based on your business requirements.
- We simulate the transaction and add the compute unit limit instruction to our transaction. We use the
getSimulationUnits
method to calculate the number of compute units our transaction will consume. We also fetch a recent blockhash for our transaction. - We sign and send our transaction to the Solana cluster.
And like that, you now have a transaction optimized for priority fees and compute units. This will increase the likelihood of your transaction being included in a block! Feel free to modify the functions to suit your needs.
Blockhash Management & Expiration Logic
Proper blockhash management is critical to figuring out what type of transaction you are crafting. The blockhash determines when your transaction expires and affects RPC node behavior during simulation and submission.
Commitment Levels
Each commitment level provides different guarantees about transaction finality:
Property | Processed | Confirmed | Finalized |
---|---|---|---|
Received block | X | X | X |
Block on majority fork | X | X | X |
Block contains target tx | X | X | X |
66%+ stake voted on block | - | X | X |
31+ confirmed blocks built atop block | - | - | X |
Source: Anza Docs - Commitments
- Solana Web3.js (Legacy)
- Solana Kit
// Recommended approach for most applications
const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash('confirmed');
// Recommended approach for most applications
const { value: latestBlockhash } = await rpc.getLatestBlockhash({ commitment: 'confirmed' }).send();
const blockhash = latestBlockhash.blockhash;
const lastValidBlockHeight = latestBlockhash.lastValidBlockHeight;
Tracking Transaction Expiration
Understanding how lastValidBlockHeight
works is crucial for proper transaction management. When you fetch a blockhash, Solana provides both the blockhash
and lastValidBlockHeight
(the maximum block height at which your transaction will still be valid).
How Blockhash Expiration Works
Solana validators maintain a BlockHashQueue
containing the 300 most recent blockhashes. However, transactions are only valid if their blockhash is within the most recent 151 stored hashes. This creates the expiration window:
- Window Size: 151 blocks from
lastValidBlockHeight
- Duration: Approximately 60-90 seconds (depending on slot timing of ~400-600ms per slot)
- Solana Web3.js (Legacy)
- Solana Kit
// When you fetch a blockhash, you can utilize the following values
const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash('confirmed');
console.log('Current block height:', await connection.getBlockHeight('confirmed'));
console.log('Transaction expires at block height:', lastValidBlockHeight);
console.log('Expiration window:', lastValidBlockHeight - await connection.getBlockHeight('confirmed'), 'blocks');
// When you fetch a blockhash, you can utilize the following values
const { value: latestBlockhash } = await rpc.getLatestBlockhash({ commitment: 'confirmed' }).send();
const blockhash = latestBlockhash.blockhash;
const lastValidBlockHeight = latestBlockhash.lastValidBlockHeight;
const blockHeight = await rpc.getBlockHeight().send();
console.log('Current block height:', blockHeight);
console.log('Transaction expires at block height:', lastValidBlockHeight);
console.log('Expiration window:', lastValidBlockHeight - blockHeight, 'blocks');
Transaction Rebroadcasting Strategies
RPC nodes have generic retry algorithms, but they're often insufficient for application-specific needs. During network congestion, implementing custom retry logic dramatically improves success rates.
Understanding RPC Default Behavior
By default, RPC nodes will:
- Rebroadcast transactions every 2 seconds
- Continue until the transaction is finalized or the blockhash expires
- Drop transactions if the queue exceeds 10,000 pending transactions
This generic approach doesn't account for your application's specific requirements or current network conditions.
Manipulating maxRetries Field
During high congestion, set maxRetries = 0
and implement your own rebroadcasting algorithm:
- Solana Web3.js (Legacy)
- Solana Kit
// Set maxRetries = 0 for manual control
const signature = await connection.sendRawTransaction(
transaction.serialize(),
{
skipPreflight: false,
maxRetries: 0
}
);
// Set maxRetries = 0 for manual control
const signature = await rpc
.sendTransaction(serializedTransaction, {
preflightCommitment: 'confirmed',
encoding: 'base64',
skipPreflight: false,
maxRetries: 0n
})
.send();
A Retry Pattern Code Example
Constant Interval Rebroadcasting
- Solana Web3.js (Legacy)
- Solana Kit
import {
Keypair,
Connection,
LAMPORTS_PER_SOL,
SystemProgram,
Transaction,
} from "@solana/web3.js";
import { sha512 } from "@noble/hashes/sha2.js";
import * as ed from "@noble/ed25519";
import { setTimeout as delay } from "timers/promises";
ed.hashes.sha512 = sha512;
const BUFFER = 10;
const { secretKey, publicKey } = await ed.keygenAsync();
const secretKey64 = new Uint8Array(64);
secretKey64.set(secretKey, 0);
secretKey64.set(publicKey, 32);
const payer = Keypair.fromSecretKey(secretKey64);
const toAccount = Keypair.generate().publicKey;
console.log("💧 Airdropping 1 SOL to payer...");
const connection = new Connection("https://api.devnet.solana.com", "confirmed");
const airdropSignature = await connection.requestAirdrop(
payer.publicKey,
LAMPORTS_PER_SOL,
);
await connection.confirmTransaction(
{ signature: airdropSignature },
"confirmed",
);
const blockhashResponse = await connection.getLatestBlockhash();
const lastValidBlockHeight = blockhashResponse.lastValidBlockHeight - BUFFER;
const transaction = new Transaction({
feePayer: payer.publicKey,
blockhash: blockhashResponse.blockhash,
lastValidBlockHeight,
}).add(
SystemProgram.transfer({
fromPubkey: payer.publicKey,
toPubkey: toAccount,
lamports: 1_000_000,
}),
);
const message = transaction.serializeMessage();
const signature = ed.sign(message, secretKey);
transaction.addSignature(payer.publicKey, Buffer.from(signature));
const rawTransaction = transaction.serialize();
let blockheight = await connection.getBlockHeight();
let attempts = 0;
let landed = false;
// Retry loop
while (blockheight < lastValidBlockHeight && !landed) {
attempts++;
console.log(
`\n📤 Attempt #${attempts} — current block: ${blockheight}, cutoff: ${lastValidBlockHeight}`,
);
try {
const signature = await connection.sendRawTransaction(rawTransaction, {
skipPreflight: true,
maxRetries: 0,
});
console.log(`📝 Sent tx, signature: ${signature}`);
// Poll for confirmation after sending
const status = await connection.getSignatureStatus(signature, {
searchTransactionHistory: false,
});
console.log("🔎 Status check:", status.value);
if (
status.value?.confirmationStatus === "confirmed" ||
status.value?.confirmationStatus === "finalized"
) {
console.log(`✅ Transaction confirmed after ${attempts} attempt(s)!`);
landed = true;
break;
}
} catch (error) {
console.error("Error sending transaction:", error);
}
await delay(500);
blockheight = await connection.getBlockHeight();
}
if (!landed) {
console.warn("Transaction not confirmed before blockhash expired.");
}
import {
Instruction,
Rpc,
SolanaRpcApi,
pipe,
createTransactionMessage,
appendTransactionMessageInstructions,
setTransactionMessageFeePayerSigner,
setTransactionMessageLifetimeUsingBlockhash,
getBase64EncodedWireTransaction,
createSolanaRpc,
lamports,
generateKeyPairSigner,
getAddressFromPublicKey,
signTransactionMessageWithSigners,
createDefaultRpcTransport,
createRpc,
RpcTransport,
createJsonRpcApi,
RpcRequest,
airdropFactory,
createSolanaRpcSubscriptions
} from "@solana/kit";
import {
getSetComputeUnitLimitInstruction,
getSetComputeUnitPriceInstruction,
} from "@solana-program/compute-budget";
import {
getTransferSolInstruction,
} from "@solana-program/system";
import {
EstimatePriorityFeesResponse,
SolanaKitEstimatePriorityFeesParams
} from "./types";
// Types for QuickNode priority fee estimation
type PriorityFeeApi = {
qn_estimatePriorityFees(params: SolanaKitEstimatePriorityFeesParams): EstimatePriorityFeesResponse;
};
// Helper function to create QuickNode RPC transport
function createQuickNodeTransport({ endpoint }: { endpoint: string }): RpcTransport {
const jsonRpcTransport = createDefaultRpcTransport({ url: endpoint });
return async <TResponse>(...args: Parameters<RpcTransport>): Promise<TResponse> => {
return await jsonRpcTransport(...args);
};
}
// Create priority fee API client
function createPriorityFeeApi(endpoint: string): Rpc<PriorityFeeApi> {
const api = createJsonRpcApi<PriorityFeeApi>({
requestTransformer: (request: RpcRequest<any>) => {
const transformedRequest = {
...request,
params: request.params[0]
};
return transformedRequest;
},
responseTransformer: (response: any) => response.result,
});
const transport = createQuickNodeTransport({ endpoint });
return createRpc({ api, transport });
}
// Retry logic with blockheight-based boundary
async function sendTransactionWithRetries(
rpc: Rpc<SolanaRpcApi>,
serializedTransaction: string,
lastValidBlockHeight: bigint,
maxRetryBuffer: number
): Promise<string> {
const maxRetryBlockHeight = lastValidBlockHeight - BigInt(maxRetryBuffer);
console.log(`=== RETRY CONFIGURATION ===`);
console.log(`Last valid block height: ${lastValidBlockHeight}`);
console.log(`Max retry block height (with ${maxRetryBuffer} buffer): ${maxRetryBlockHeight}`);
let attempt = 0;
let lastError: any = null;
while (true) {
attempt++;
// Get current block height
const currentBlockHeight = await rpc.getBlockHeight({ commitment: 'confirmed' }).send();
console.log(`Attempt ${attempt}: Current block height: ${currentBlockHeight}`);
// Check if we've exceeded our retry boundary
if (currentBlockHeight > maxRetryBlockHeight) {
console.log(`❌ Retry boundary exceeded. Current: ${currentBlockHeight}, Max retry: ${maxRetryBlockHeight}`);
throw new Error(`Transaction retry timeout: exceeded block height boundary (${currentBlockHeight} >= ${maxRetryBlockHeight})`);
}
console.log(`📤 Sending transaction attempt ${attempt}...`);
// Send the transaction
const signature = await rpc.sendTransaction(serializedTransaction as any, {
encoding: 'base64',
skipPreflight: true,
maxRetries: 0n // We handle retries ourselves
}).send();
console.log(`✅ Transaction sent successfully on attempt ${attempt}!`);
console.log(`Signature: ${signature}`);
// Wait a bit for confirmation
console.log('Waiting for transaction confirmation...');
// Check transaction status
const status = await rpc.getSignatureStatuses([signature]).send();
const signatureStatus = status.value[0];
if (signatureStatus?.confirmationStatus === 'confirmed' || signatureStatus?.confirmationStatus === 'finalized') {
console.log(`🎉 Transaction confirmed with status: ${signatureStatus.confirmationStatus}`);
return signature;
} else if (signatureStatus?.err) {
console.log(`❌ Transaction failed with error:`, signatureStatus.err);
lastError = signatureStatus.err;
} else {
console.log(`⏳ Transaction still processing, will retry...`);
await new Promise(resolve => setTimeout(resolve, 500));
}
}
}
// Simulate transaction to get compute units
async function getSimulationUnits(
rpc: Rpc<SolanaRpcApi>,
instructions: Instruction[],
payerSigner: any
): Promise<number | undefined> {
const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();
const testInstructions = [
...instructions,
];
const transactionMessage = pipe(
createTransactionMessage({ version: 0 }),
(tx) => appendTransactionMessageInstructions(testInstructions, tx),
(tx) => setTransactionMessageFeePayerSigner(payerSigner, tx),
(tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx)
);
const transaction = await signTransactionMessageWithSigners(transactionMessage);
const rawTx = getBase64EncodedWireTransaction(transaction);
try {
const simulation = await rpc.simulateTransaction(rawTx, {
commitment: 'confirmed',
encoding: 'base64',
sigVerify: false,
replaceRecentBlockhash: true,
}).send();
if (simulation.value.err != null || simulation.value.unitsConsumed == null) {
console.warn('Simulation failed:', simulation.value.err);
return undefined;
}
return Number(simulation.value.unitsConsumed);
} catch (error) {
console.error('Error during simulation:', error);
return undefined;
}
}
// Main function - Optimized transaction with priority fees and compute units
async function main() {
// Use devnet for testing (has faucet for funding accounts)
const rpc = createSolanaRpc('http://127.0.0.1:8899');
const rpcSubscriptions = createSolanaRpcSubscriptions('ws://127.0.0.1:8900');
const airdrop = airdropFactory({ rpc, rpcSubscriptions });
const QUICKNODE_ENDPOINT = "YOUR_QUICKNODE_ENDPOINT";
console.log('=== GENERATING ACCOUNTS ===');
// Generate sender and receiver accounts
const senderSigner = await generateKeyPairSigner();
const recipientSigner = await generateKeyPairSigner();
const senderAddress = await getAddressFromPublicKey(senderSigner.keyPair.publicKey);
const recipientAddress = await getAddressFromPublicKey(recipientSigner.keyPair.publicKey);
console.log('Sender:', senderAddress);
console.log('Recipient:', recipientAddress);
await airdrop({
recipientAddress: senderAddress,
lamports: lamports(1_000_000_000n),
commitment: 'confirmed',
});
// Create transfer instruction
const instructions: Instruction[] = [
getTransferSolInstruction({
source: senderSigner,
destination: recipientAddress,
amount: lamports(1_000_000n), // 0.001 SOL
}),
];
console.log('\n=== FETCHING PRIORITY FEES ===');
// Fetch priority fee using QuickNode API
const quickNodeRpc = createPriorityFeeApi(QUICKNODE_ENDPOINT);
const priorityFeesResponse = await quickNodeRpc.qn_estimatePriorityFees({
last_n_blocks: 100,
api_version: 2
}).send();
const priorityFee = priorityFeesResponse.recommended;
console.log('\n=== ADDING PRIORITY FEE INSTRUCTION ===');
// Add priority fee instruction
const priorityFeeInstruction = getSetComputeUnitPriceInstruction({
microLamports: priorityFee,
});
// Add priority fee instruction to the beginning of the instructions array
instructions.unshift(priorityFeeInstruction);
console.log(`Added priority fee instruction with ${priorityFee} microlamports`);
console.log('\n=== SIMULATING FOR COMPUTE UNITS ===');
// Simulate the transaction and add the compute unit limit instruction
const computeUnits = await getSimulationUnits(rpc, instructions, senderSigner);
if (computeUnits) {
console.log(`Transaction would consume: ${computeUnits} compute units`);
const adjustedUnits = Math.ceil(computeUnits * 1.1); // 10% margin
console.log(`Using compute units with 10% margin: ${adjustedUnits}`);
const computeUnitLimitInstruction = getSetComputeUnitLimitInstruction({
units: adjustedUnits
});
instructions.unshift(computeUnitLimitInstruction);
console.log('Added compute unit limit instruction');
} else {
console.log('Could not determine compute units from simulation, using default limit');
const computeUnitLimitInstruction = getSetComputeUnitLimitInstruction({
units: 200_000 // Default fallback
});
instructions.unshift(computeUnitLimitInstruction);
console.log('Added default compute unit limit instruction');
}
console.log('\n=== BUILDING TRANSACTION ===');
// Get latest blockhash with current block height info
const { value: latestBlockhash } = await rpc.getLatestBlockhash({ commitment: 'confirmed' }).send();
const currentBlockHeight = await rpc.getBlockHeight({ commitment: 'confirmed' }).send();
console.log('Latest blockhash:', latestBlockhash.blockhash);
console.log('Current block height:', currentBlockHeight);
console.log('Last valid block height:', latestBlockhash.lastValidBlockHeight);
// Create transaction message
const transactionMessage = pipe(
createTransactionMessage({ version: 0 }),
(tx) => appendTransactionMessageInstructions(instructions, tx),
(tx) => setTransactionMessageFeePayerSigner(senderSigner, tx),
(tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx)
);
// Sign transaction
const signedTransaction = await signTransactionMessageWithSigners(transactionMessage);
const serializedTransaction = getBase64EncodedWireTransaction(signedTransaction);
console.log('\n=== SENDING WITH RETRY LOGIC ===');
// Send transaction with retry logic
try {
const signature = await sendTransactionWithRetries(
rpc,
serializedTransaction,
latestBlockhash.lastValidBlockHeight,
10 // 10 block buffer
);
console.log('\n✅ TRANSACTION COMPLETED!');
console.log('Final Signature:', signature);
} catch (error) {
console.error('\n❌ TRANSACTION FAILED AFTER ALL RETRIES');
console.error('Error:', error);
}
}
main().catch(console.error);
Let's break down this retry pattern implementation step by step:
1. Setup and Dependencies
import { setTimeout as delay } from "timers/promises";
const BUFFER = 10;
const { secretKey, publicKey } = await ed.keygenAsync();
const secretKey64 = new Uint8Array(64);
secretKey64.set(secretKey, 0);
secretKey64.set(publicKey, 32);
const payer = Keypair.fromSecretKey(secretKey64);
We define a delay utility and a buffer to reduce the expiration window slightly, accounting for network delays. We also convert Noble's key generation result to be compatible with Solana's Keypair object, which requires a 64-byte Uint8Array
containing both the 32-byte secret key and 32-byte public key.
2. Transaction Preparation
const blockhashResponse = await connection.getLatestBlockhash();
const lastValidBlockHeight = blockhashResponse.lastValidBlockHeight - BUFFER;
We fetch a fresh blockhash and reduce the lastValidBlockHeight
by our buffer to ensure we stop retrying before the actual expiration.
3. Transaction Construction and Signing
const transaction = new Transaction({
feePayer: payer.publicKey,
blockhash: blockhashResponse.blockhash,
lastValidBlockHeight,
}).add(/* instructions */);
const message = transaction.serializeMessage();
const signature = ed.sign(message, secretKey);
transaction.addSignature(payer.publicKey, Buffer.from(signature));
We build the transaction with our blockhash and manually sign it. Pre-signing allows us to reuse the same transaction bytes for multiple submission attempts.
4. Retry Loop with Expiration Checking
while (blockheight < lastValidBlockHeight && !landed) {
// Continue retrying while transaction is still valid and not confirmed
}
The loop continues as long as:
- Current block height is below our expiration threshold (
lastValidBlockHeight - BUFFER
) - Transaction hasn't been confirmed yet
5. Transaction Submission with Error Handling
const signature = await connection.sendRawTransaction(rawTransaction, {
skipPreflight: true, // Skip simulation to avoid delays
maxRetries: 0 // Disable retry - we handle it manually
});
We use skipPreflight: true
and maxRetries: 0
for maximum control over the retry process.
6. Immediate Confirmation Checking
const status = await connection.getSignatureStatus(signature, {
searchTransactionHistory: false // Faster lookup, recent transactions only
});
After sending, we immediately check if the transaction was confirmed rather than waiting for the next retry cycle.
7. Controlled Retry Timing
await delay(500); // 500ms between attempts
blockheight = await connection.getBlockHeight();
We wait 500ms
between attempts (faster than the default 2-second RPC retry) and update our block height for the next iteration.
Key Benefits of This Pattern:
- Fast retry cycles: 500ms intervals vs default 2-second RPC retry
- Manual control: Full visibility into retry attempts and timing
- Early termination: Stops immediately when transaction confirms
- Safety buffer: Accounts for network processing delays
Use Stake-Weighted Quality of Service (SWQoS)
Stake-Weighted Quality of Service (SWQoS) is a newer mechanism to increase the likelihood of your transaction being included in a block. QuickNode provides a variety of SWQoS options:
- Using Smart Transactions (or manually building the transaction following best practices) with a priority fee API setting of
recommended
or higher will use stake from QuickNode's own stake pool, and do the proper assembly of compute and priority fees for the user. Any user sending non-duplicate transactions withrecommended
priority fees or higher will now be routed through QuickNode's own stake pool. - Using Jito bundles, i.e. (Lil' JIT's
sendTransaction
orsendBundle
methods) will use Jito's own stake pool, which has significant stake in the Solana network. - For ultra-high priority transactions, including professional traders and high-frequency automated trading, Fastlane, enabled in the QuickNode marketplace, is a SWQoS option using a stake pool from the top 1% largest validators in Solana, and is the absolute best way to increase your transaction landing rate.
QuickNode SDK
If you want to streamline the process of assembling Solana transactions, you can use the QuickNode SDK to create and send optimized transactions! Note: you will need the Solana Priority Fees Add-on to use the relevant methods in the SDK.
To use the SDK, you will need to install it in your project:
npm i @quicknode/sdk # or yarn add @quicknode/sdk
There are three relevant methods in the SDK that you can use to create and send optimized transactions:
sendSmartTransaction
- This method will create and send a transaction with priority fees and optimized compute units and a given Keypair.prepareSmartTransaction
- This method will prepare a transaction with priority fees and optimized compute units.fetchEstimatePriorityFees
- This method will fetch the recent priority fees using theqn_estimatePriorityFees
add-on method.
Here's a sample of how you can use the SDK to send a "smart" transaction to the Solana cluster:
import { solanaWeb3, Solana } from "@quicknode/sdk";
const { Transaction, SystemProgram, Keypair, PublicKey } = solanaWeb3;
const mainSecretKey = Uint8Array.from([
// Replace with your secret key
]);
const sender = Keypair.fromSecretKey(mainSecretKey);
const receiver = new PublicKey("YOUR_RECEIVER_ADDRESS");
const senderPublicKey = sender.publicKey;
const endpoint = new Solana({
endpointUrl:
"https://some-cool-name.solana-mainnet.quiknode.pro/redacted",
});
const transaction = new Transaction();
// Add instructions for each receiver
transaction.add(
SystemProgram.transfer({
fromPubkey: senderPublicKey,
toPubkey: receiver,
lamports: 10,
})
);
(async () => {
// Endpoint must added to Priority Fee API to do this
const signature = await endpoint.sendSmartTransaction({
transaction,
keyPair: sender,
feeLevel: "recommended"
});
console.log(signature);
})().catch(console.error);
In this example, we use the sendSmartTransaction
method to create and send a transaction with priority fees and optimized compute units. We also use the SystemProgram.transfer
instruction to transfer lamports from the sender to the receiver. You can customize the feeLevel
based on your business requirements. That's it!
Wrap Up
We've covered how to optimize a Solana transaction using priority fees, compute optimization, proper blockhash management, and custom rebroadcasting strategies to achieve high transaction success rates. By combining these techniques with tools like the QuickNode SDK, you can increase the likelihood of your transaction being included in a block.
Check out QuickNode Marketplace to integrate our Solana Priority Fees Add-on and explore other tools to improve your business operations.
If you have a question or idea you want to share, drop us a line on Discord or X (Formerly Twitter)!
We ❤️ Feedback!
Let us know if you have any feedback or requests for new topics. We'd love to hear from you.