Skip to main content

Strategies to Optimize Solana Transactions

Updated on
Oct 15, 2024

Optimizing transactions on the Solana network 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.

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 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.

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

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"
}
}'

Priority Fees Resources

Optimize Compute Units

Optimizing compute units (CU) helps prevent transactions from being dropped due to exceeding block limits. Every transaction on Solana uses 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. Additionally, priority fees are a function of compute units, so lower compute means savings on fees.

As of October 2024, the following limits/defaults are in place:

LimitsCompute Units
Max Compute per block48 million
Max Compute per account per block12 million
Max Compute per transaction1.4 million
Transaction Default Compute200,000

There are two main ways you can optimize compute units:

  1. On your client-side, you can simulate the transaction to determine the compute units it will consume and set the limit accordingly.
  2. In your Solana programs, you can design your programs to be compute-conscious to limit unnecessary computation when your program is invoked.

Setting Compute Unit Limits

To set a compute unit limit for your transaction, you can use the setComputeUnitLimit instruction. This value is what is evaluated in a block's compute limits.

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

Calculating the Right Compute Units

To estimate a transaction's expected compute units, you can use the simulateTransaction method to simulate the transaction and determine the compute units it will consume. To make sure you calculate the correct compute, your simulated transaction must include a setComputeUnitLimit instruction. Use simulateTransaction to determine the compute units a transaction will consume:

const testTransaction = new Transaction().add(testInstruction);
const simulation = await connection.simulateTransaction(testTransaction, {
replaceRecentBlockhash: true,
sigVerify: false,
});
const targetComputeUnitsAmount = simulation.value.unitsConsumed;

Efficient Program Design

If you are writing Solana programs, you should be aware that your program code can contribute significantly to the compute units consumed by a transaction. To optimize your programs, you can use the following techniques:

  • Minimize unnecessary logging (if you must log a key, make sure to use .key().log() instead of just .key() because base58 encoding is expensive)
  • User smaller data types (e.g., use u8 instead of u64 if appropriate for your use case)
  • Utilize Zero Copy for large data structures (this will result in lower CU for serializing and deserializing the data)
  • Save bumps into PDAs. By saving your bumps you can reduce the compute required to find_program_address

Compute Units Resources

Transaction Assembly Best Practices

Combine priority fees and compute unit optimization in your transaction assembly:

  1. Create a transaction with your instructions
  2. Fetch and add priority fees
  3. Add a priority fee instruction to the transaction
  4. Simulate the transaction with a compute unit limit instruction
  5. Add a compute unit limit instruction to the transaction with the computed limit from the simulation
  6. Fetch and add a recent blockhash to the transaction
  7. Sign and send the transaction

Check out our sample code here.

Utilize QuickNode SDK - Smart Transaction

The QuickNode SDK provides methods for handling the above steps for your:

  • sendSmartTransaction: Creates and sends a transaction with optimized settings.
  • prepareSmartTransaction: Prepares an optimized transaction.

Example usage:

const signature = await endpoint.sendSmartTransaction({
transaction,
keyPair: sender,
feeLevel: "high"
});

Check out our docs for more details.

Use Jito Bundles

For advanced transaction optimization and bundling, consider using the Lil' JIT marketplace 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 base58 strings:
const transactionEncoder = getTransactionEncoder();
const base58Decoder = getBase58Decoder();

const base58EncodedTransactions = signedTransactions.map((transaction) => {
const transactionBytes = transactionEncoder.encode(transaction);
return base58Decoder.decode(transactionBytes) as Base58EncodedBytes;
});
  1. Send the transaction(s) to the the Jito validator client using sendTransaction or sendBundle methods:
const bundleId = await lilJitRpc
.sendBundle(base58EncodedTransactions)
.send();

Transaction confirmation

To ensure your transaction has landed in a block, you should utilize the getSignatureStatuses method to check the status of your transaction:

const signature = await connection.sendTransaction(transaction, [signer]);
const { value: statuses } = await connection.getSignatureStatuses([signature]);

Create a simple polling function to check on the status of your transaction. If it is not confirmed after 150 slots (about 60 seconds), your blockhash will expire, and you can retry the transaction.

We ❤️ Feedback!

If you have any feedback or questions about this documentation, let us know. We'd love to hear from you!

Share this doc