Skip to main content

Strategies to Optimize Solana Transactions

Updated on
Oct 21, 2026

Optimizing transactions on Solana is crucial for ensuring their inclusion in blocks, especially during periods of high network traffic. This document outlines key strategies for improving transaction performance and reliability.

This guide is available in Solana Web3.js v3, Solana Kit, and Solana Web3.js v1 (Legacy) versions. Select the appropriate tab to view the code snippets and instructions for your preferred library:

note

The explanations throughout this guide primarily reference the Solana Web3.js (Legacy) code examples. The Solana Web3.js v3 examples use the same code with v3's changes applied: key generation, signing, and legacy Transaction.serialize() are awaited, RPC values such as block heights are bigint, and maxRetries takes a bigint. The Solana Kit implementations follow the same conceptual patterns and logic with updated syntax and method calls.

Install the library for the tab you follow:

# Solana Web3.js (v3.x)
npm install @solana/web3.js@3
# Solana Web3.js (Legacy)
npm install @solana/web3.js@1

The retry example in A Retry Pattern Code Example also uses @noble/ed25519 and @noble/hashes. For everything that changed between v1 and v3, see How to Migrate from Solana web3.js v1 to v3.

There are several strategies you can take to increase the likelihood of your transaction being included in a block and confirmed by the network quickly. These include:


Use Priority Fees​

Solana's priority fees allow 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.

Priority fees can be added to your transaction by including a setComputeUnitPrice instruction in your transaction:

Version 1 transactions

The examples in this section build legacy and version 0 transactions. A version 1 transaction sets its priority fee in the message's transactionConfig instead, as an absolute lamport amount for the whole transaction rather than a microlamport price per compute unit. A setComputeUnitPrice instruction in a version 1 transaction is accepted but has no effect. See Version 1 Transactions.

import { Transaction, ComputeBudgetProgram } from '@solana/web3.js'

const transaction = new Transaction()
// Add your transaction instructions here
const priorityFeeInstruction = ComputeBudgetProgram.setComputeUnitPrice({
microLamports: priorityFeeAmount,
})
transaction.add(priorityFeeInstruction)

Calculating the Right Priority Fee​

Quicknode provides a Priority Fee API (Add-on Details | Docs) to fetch recent priority fees for a given Program over a specified number of blocks.

curl https://docs-demo.solana-mainnet.quiknode.pro/ \
-X POST \
-H "Content-Type: application/json" \
-H "x-qn-api-version: 1" \
--data '{
"jsonrpc": "2.0",
"id": 1,
"method": "qn_estimatePriorityFees",
"params": {
"last_n_blocks": 100,
"account": "JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4",
"api_version": 2
}
}'

The method returns an object with a per_compute_unit property that includes estimates for priority fees (in microlamports) based on recent network activity. The response provides fee estimates in four categories: low, medium, high, and extreme. Additionally, the response includes a recommended property that provides a recommended fee level for each compute unit based on the current network conditions.

These estimates are priced per compute unit in microlamports, which is what setComputeUnitPrice expects on a legacy or version 0 transaction. A version 1 transaction sets one absolute fee for the whole transaction, so multiply the per-compute-unit estimate by your computeUnitLimit and divide by 1,000,000 to get the priorityFee in lamports.


Priority Fees Resources​


Optimize Compute Units​


On your client-side, you can simulate the transaction to determine the compute units it will consume and set the limit accordingly.

To set a compute unit limit on a legacy or version 0 transaction, you can use the setComputeUnitLimit instruction. This value is what is evaluated in a block's compute limits. Version 1 transactions set the limit in transactionConfig instead, covered in Version 1 Transactions below.

const testTransaction = new Transaction().add(testInstruction)
const computeUnitInstruction = ComputeBudgetProgram.setComputeUnitLimit({
units: targetComputeUnitsAmount,
})
testTransaction.add(computeUnitInstruction)

Version 1 Transactions​

Version 1 transactions do not use ComputeBudget instructions. Every compute and fee setting moves onto the message itself, into a transactionConfig object:

FieldRequiredValue
computeUnitLimitYesThe maximum compute units the transaction may consume
loadedAccountsDataSizeLimitYesThe maximum total size in bytes of the account data the transaction may load
priorityFeeNoAn absolute lamport amount for the whole transaction
heapSizeNoThe requested heap size in bytes

Both limits are mandatory in practice, because an unset value is zero and there is no 200,000 compute unit default to fall back on. Omit computeUnitLimit and the transaction fails with ComputationalBudgetExceeded; omit loadedAccountsDataSizeLimit and it fails with MaxLoadedAccountsDataSizeExceeded.

ComputeBudget instructions remain valid instructions in a version 1 transaction, so including them raises no error, but they set nothing. They only consume the compute units it costs to run them.

Solana Kit writes transactionConfig through dedicated setters, available from @solana/kit v7.0.0:

import {
pipe,
createTransactionMessage,
appendTransactionMessageInstructions,
setTransactionMessageComputeUnitLimit,
setTransactionMessageLoadedAccountsDataSizeLimit,
setTransactionMessagePriorityFeeLamports,
lamports,
} from "@solana/kit";

const transactionMessage = pipe(
createTransactionMessage({ version: 1 }),
(tx) => appendTransactionMessageInstructions(yourInstructions, tx),
(tx) => setTransactionMessageComputeUnitLimit(400_000, tx),
(tx) => setTransactionMessageLoadedAccountsDataSizeLimit(64 * 1024, tx),
(tx) => setTransactionMessagePriorityFeeLamports(lamports(10_000n), tx)
);

Solana Web3.js v3 builds the same message from a TransactionMessage with compileToV1Message(), which takes the limits and fee directly. Solana Web3.js v1 (Legacy) can read version 1 transactions but cannot build them, so there is no legacy equivalent:

import { TransactionMessage, VersionedTransaction } from "@solana/web3.js";

const message = new TransactionMessage({
payerKey: payer.publicKey,
recentBlockhash: blockhash,
instructions: yourInstructions,
}).compileToV1Message({
computeUnitLimit: 400_000,
loadedAccountsDataSizeLimit: 64 * 1024,
priorityFeeLamports: 10_000n,
});
const transaction = new VersionedTransaction(message);
await transaction.sign([payer]);

Simulating for compute units works the same way as it does for version 0, with one difference: give the simulated transaction a computeUnitLimit high enough to complete, then set the measured value plus a margin on the transaction you send.

A version 1 transaction can be up to 4,096 bytes, against 1,232 for legacy and version 0. Base58 is capped at 1,232 decoded bytes whatever the version, so anything larger has to be submitted as base64.

Compute Units Resources​


Transaction Assembly Best Practices​

Refer to the code below that combines priority fees and compute unit optimization during transaction building. Both examples build a version 0 transaction; to build the same thing as version 1, replace the two ComputeBudget instructions with the transactionConfig setters shown in Version 1 Transactions:

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

async function main(){
// Key generation is async in web3.js v3
const keyPair = await Keypair.generate(); // derive your keypair from your secret key with await Keypair.fromSecretKey(...)

// 1. Establish a connection to the Solana cluster
const connection = new Connection(endpoint, 'finalized');

// 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 (signing and serializing are async in web3.js v3)
transaction.feePayer = keyPair.publicKey;
transaction.recentBlockhash = recentBlockhash.blockhash;
await transaction.sign(keyPair);

const hash = await connection.sendRawTransaction(
await transaction.serialize(),
{ skipPreflight: true, maxRetries: 0n }
);

return hash;
}

Blockhash Management & Expiration​

Solana transactions include a recent blockhash, which validators use to ensure the transaction is both fresh and unique. This mechanism prevents replay attacks, where the same transaction could be resubmitted multiple times. Blockhashes expire after approximately 151 blocks (about 30 seconds at ~200 ms per block), after which any transaction referencing them becomes invalid. If a transaction is not finalized within this window, it must be rebuilt with a new blockhash before being retried.

Commitment Levels​

Commitment levels describe a block's progress. Under Alpenglow, confirmed and finalized provide the same guarantees:

PropertyProcessedConfirmedFinalized
Received blockXXX
Block contains target txXXX
60%+ stake voted finalize or 80%+ stake voted notarize on block-XX
Target tx is committed to the permanent ledger-XX

Source: Anza Docs - Commitments

Under Alpenglow, confirmed and finalized provide equivalent commitment guarantees. Use finalized explicitly; confirmed remains supported for compatibility but is scheduled for removal in a later release. See Solana’s Alpenglow upgrade page.

Transaction Retry Strategies​

Implement custom retry logic for better transaction success rates during network congestion. The retry logic itself is the same for every transaction version; only the compute and fee setup differs, as described in Version 1 Transactions.

The maxRetries Strategy​

Take manual control of transaction rebroadcasting during network congestion.

// Disable RPC retry for manual control (maxRetries is a bigint in web3.js v3)
const signature = await connection.sendRawTransaction(
await transaction.serialize(),
{
skipPreflight: false,
maxRetries: 0n
}
);

A Retry Pattern Code Example​

Complete implementation showing constant interval rebroadcasting with expiration handling.

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 = 10n; // block heights are bigint in web3.js v3

const { secretKey, publicKey } = await ed.keygenAsync();
const secretKey64 = new Uint8Array(64);
secretKey64.set(secretKey, 0);
secretKey64.set(publicKey, 32);

const payer = await Keypair.fromSecretKey(secretKey64);
const toAccount = (await Keypair.generate()).publicKey;

console.log("šŸ’§ Airdropping 1 SOL to payer...");
const connection = new Connection("https://api.devnet.solana.com", "finalized");
const airdropSignature = await connection.requestAirdrop(
payer.publicKey,
LAMPORTS_PER_SOL,
);
await connection.confirmTransaction(
{ signature: airdropSignature, ...(await connection.getLatestBlockhash()) },
"finalized",
);

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 = await transaction.serialize();

let blockheight = await connection.getBlockHeight();
let attempts = 0;
let landed = false;
let executionError = null;

// 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: 0n,
});
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?.err) {
executionError = status.value.err;
break;
}

if (status.value?.confirmationStatus === "finalized") {
console.log(`āœ… Transaction finalized after ${attempts} attempt(s)!`);
landed = true;
break;
}
} catch (error) {
console.error("Error sending transaction:", error);
}

await delay(500);
blockheight = await connection.getBlockHeight();
}

if (executionError) {
throw new Error(`Transaction execution failed: ${JSON.stringify(executionError)}`);
}

if (!landed) {
console.warn("Transaction not finalized before blockhash expired.");
}

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 with recommended priority fees or higher will now be routed through Quicknode's own stake pool.

  • Using Jito bundles, i.e. (Lil' JIT's sendTransaction or sendBundle methods) will use Jito's own stake pool, which has significant stake in the Solana network.

Use Jito Bundles​

For advanced transaction optimization and bundling, consider using the Lil' JIT add-on. This add-on enables the creation of Jito Bundles, allowing for atomic execution of multiple transactions.

The add-on enables you to provide a tip to Jito validator producing the next block to prioritize your transactions (or bundles of transactions). To use the add-on, you must:


  1. Include a SOL transfer instruction in your transaction (or last transaction if you are bundling multiple transactions) to a Jito Tip Account (accesible via getTipAccounts method):
const tipAccounts = await rpc.getTipAccounts().send()
const jitoTipAddress =
tipAccounts[Math.floor(Math.random() * tipAccounts.length)]

  1. Serialize your transaction(s) into base64 strings. Base58 is capped at 1,232 decoded bytes, so a version 1 transaction above that size can only be submitted as base64:
const base64EncodedTransactions = signedTransactions.map(transaction =>
getBase64EncodedWireTransaction(transaction)
) as Base64EncodedWireTransaction[]

  1. Send the transaction(s) to the the Jito validator client using sendTransaction or sendBundle methods:
const bundleId = await lilJitRpc.sendBundle(base64EncodedTransactions).send()

Wrapping Up​

For a comprehensive overview of Solana transaction optimization with detailed explanations, check out our complete transaction optimization guide.