Skip to main content

How to Improve Solana Transaction Performance

Created on
Updated on
Oct 29, 2024

12 min read

Overview

Recent (Q1 2024) demand for Solana block space has seen an increase network traffic and subsequently in dropped transactions. The primary cause is some stress testing of Solana's priority fee system and transaction schedule (planned upgrades for April 2024 are expected to address these issues). Despite these challenges, the network continues to produce blocks and process transactions. Still, there are tools to employ in your own applications to improve performance and increase the likelihood of your transaction inclusion in a block. In this guide, we will discuss two methods to consider in your transaction assembly that will improve the likelihood of a transaction being settled on-chain:


  • Implement Priority Fees
  • Optimize Compute Units

Let's jump in!

Before jumping into the guide, if you prefer a video walkthrough? Follow along with Sahil to learn how to optimize your transactions using priority fees and QuickNode SDK.
Subscribe to our YouTube channel for more videos!

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 Guide, How to Use Priority Fees on Solana. Right now, 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, and extreme). Here is an example of how you can fetch the latest fees in your TypeScript application:

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;
}

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,
}
}
},
"id": 1
}

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 March 2024, the following limits/defaults are in place:

  • Max Compute per block: 48 million CU (during contested periods, this limit is often reached)
  • Max Compute per account per block: 12 million CU
  • Max Compute per transaction: 1.4 million CU
  • Transaction Default Compute: 200,000 CU
  • Cost per Transaction: 5000 lamports per signature (accessible via getFeeForMessage RPC method)
  • Incremental cost per CU: 0 (this may change in the future)

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 method from the Solana Web3.js library. Here is an example of how you can use this method in your TypeScript application:

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;
}

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.

Now, we have a method to calculate the number of compute units our transaction will consume. Let's use this and our fetchEstimatePriorityFees method to create a transaction assembly that will consider recent network fees and the number of compute units our transaction will consume.

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:

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.per_compute_unit['medium']; // Replace with your priority fee level based on your business requirements

// 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;
}

Let's break down the steps in our example:

  1. We establish a connection to the Solana cluster.
  2. We create our transaction. This is where you would add your instructions to the transaction.
  3. We fetch the recent priority fees using our fetchEstimatePriorityFees method.
  4. We create a priority fee instruction and add it to our transaction. You can customize your priority fee level based on your business requirements.
  5. 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.
  6. 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!

QuickNode SDK

If you want to streamline this process a bit, 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 the qn_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: "high"
});
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!

Getting Started with QuickNode

Connect to a Solana Cluster with Your QuickNode Endpoint

To build on Solana, you'll need an API endpoint to connect with the network. You're welcome to use public nodes or deploy and manage your own infrastructure; however, if you'd like 8x faster response times, you can leave the heavy lifting to us. See why over 50% of projects on Solana choose QuickNode and sign up for a free account here. We're going to use a Solana Mainnet endpoint.

Copy the HTTP Provider link:

Wrap Up

Until Solana 1.18 upgrades are rolled out, landing transactions may be a bit more challenging. However, by implementing priority fees and optimizing your transaction's compute units, 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 Twitter!

We ❤️ Feedback!

Let us know if you have any feedback or requests for new topics. We'd love to hear from you.

Resources

Share this guide