TL;DR ⚡

  • What you’ll learn: How to reduce Solana transaction Compute Units through six practical, real-world code optimizations. We take an instruction consuming twenty three thousand CUs and shrink it to fifteen thousand CUs with clear examples and explanations.

  • Why it matters: Lower CU usage means cheaper transactions, faster confirmations, and more reliable inclusion in blocks. Understanding where Anchor spends CUs helps you write lighter, more predictable instructions and avoid unnecessary bottlenecks.

  • Exo Edge: At Exo, we constantly benchmark CU usage across programs we build. These optimizations reflect the same discipline we apply when designing institutional-grade Solana systems, ensuring tighter performance, lower fees, and smoother execution for any client building on our stack.

Introduction

Compute Units (CUs) are a metric for how “heavy” a Solana transaction is — think of them as a “processor cost.” They matter because transaction fees depend (in part) on how many CUs are used. The fewer CUs, the lower the fee. Also, a transaction that demands fewer CUs is more easily included in a block, and thus confirmed faster.

We will show in this blog how little improvements / good practices in your code can reduce used CUs. We will take an artificial instruction that consumes 23K CUs, and will shrink it down to 15K CUs with a few updates that we will explain.

How does the CUs impact the transaction fees?

Transaction fees are the sum of:

  1. Base transaction fee (5000 lamports)

  2. Priority fees: Used CUs x lamports per CU

We can easily see here that reducing CUs used by an instruction directly impacts how much priority fees you will pay.

Let’s Check The code

1. Initial state - 23.5 CUs

The instruction, which creates a PDA and performs a token transfer initially consumes 23.5K CUs, you can find the code here - step 0.

2. First optimization:

Using zero copy: 450 CUs saved

This first optimization is not as relevant here as it could be with another context.

When using regular anchor accounts, every time an account is passed in an instruction anchor tries to deserialize it, and copy its data. This could have a big cost, if the account holds a lot of data. In our example the struct of the PDA is quite simple, hence the impact is quite small, but we wanted to explain the principle.

We can replace regular account by zero copy account. Those accounts are not deserialized (and data are not copied) by anchor by default. It involves a few changes in the code, but the impact could be really impressive. You can find information about zero copy here.

You can find instructions’ code here - step 1

3. Second optimization:

Do not deserialize token accounts: 2K CUs saved

In your instruction structure, when you use pub ata_to: Account<'info, TokenAccount> it tells Anchor to deserialize this account, meaning Anchor will perform a read on the data in order to format them. In some cases it could be helpful (reading a balance, for example), but in our case, since we don’t use this account except in the CPI, we don’t need to deserialize it.

What about security? If we don’t check the account, then we can send any account instead of a token account, right? Yes and no. Yes you can send any account, and this won’t trigger an error, in our program. But since SPL token program is performing those checks, we don’t need to perform them as well. Hence, there is no security flaw here.

We’ve updated this:

from :

#[account(
        mut, 
        constraint = ata_to.mint == mint.key()
    )]
    pub ata_to: Account<'info, TokenAccount>,

to:

#[account(
        mut, 
    )]
    pub ata_from: UncheckedAccount<'info>,

You can find the code here - step 2.

4. Third optimization:

Do not deserialize mint account: 750 CUs saved

This optimization is really similar to the previous one, and has the same kind of impact, and same security aspects.

BUT, there is one difference: in the previous optimization, we could do it without updating anything in our code except the instruction’s structure.

Here, it’s a bit different, since we actually need some information in the mint account’s data (number of decimals). Do we need to deserialize it to read decimals? No. We just need to access the data, and know the structure. It happens that in a mint account, decimals is a u8 stored at position 44. Hence, we can read decimals like this:

let decimals = ctx.accounts.mint.data.borrow()[44];

We only read what we need; we don’t need to deserialize everything in the account.

You can find the code here - step 3

5. Fourth optimization:

Do not check seeds: 1.7K CUs saved

As you can see, in our structure, we have a “seed” constraint. It tells to Anchor: (please) derive the address with the following seeds, and check that the provided account address is correct with those seeds. This optimization is not always a no brainer: sometimes it could be important to add a seed constraint. Sometimes it is not:

  1. If your backend is signing the transaction for example, then you can (i assume) trust yourself.

  2. If we are using an existing PDA, in your code there might be checks on the PDA’s data, which implicitly validate the seeds. It is worth noting that this is quite uncommon since in PDA seeds there usually is a string (usually the name or a keyword for the PDA). For example: let’s say seeds are “pda” (string) + user’s pubkey, and the user’s pubkey is also saved in the data of the PDA. If there is a check on this data (the user’s pubkey), then you don’t need to check the seeds since it is implied that they are correct.

  3. Let’s say you have a config PDA account, and there could be only 1 for the whole program. In this case you can check the seeds only when initializing the account, and in all other instructions you can omit this check, since there is only 1 account and the discriminator will be checked, we’re 100% sure that the provided account has the right address.

You can find the code here - step 4

Although this optimization seems simple and really efficient, you have to understand 200% what you’re doing before removing the seeds check, since it can lead to a security breach.

6. Fifth optimization:

Send random pubkey as an argument: 230 CUs saved

As you can see in our code, we use the random pubkey in the seeds of the counter PDA & we save it in the PDA’s data.

Currently we get the random pubkey address by sending it as an account. However, we don’t need to have it as an account, because we just don’t use it. We just need the pubkey address. Thus, by providing the address as an argument to the instruction, we save some CUs because anchor don’t need to read / deserialize the account’s data.

You can find the code here - step 5

7. Sixth optimization:

Removing the msg!(): 2.9K CUs saved (❗❗❗)

This is by far the easiest optimization, the least risky, and one of the most impactful.

msg!() is used to log data in the transaction’s log. Most of the time, this is used only for debugging.

Before going into production, just remove all the msg!(), if they are not really needed.

You can find the code here - step 6

Summary

When we initially coded the initial state, we deliberately created a validate_counter function that use a counter account as an argument, which is not a reference. We assumed that not using a reference was implying using more CUs, but it actually was not the case.

In conclusion, the purpose of this blog is not to provide a 100% optimized instruction, but rather to show that with a few optimizations we can drastically reduce CUs used (by +/- 35%!). There are probably other optimizations that we haven’t thought of, but the idea here is to make you think of “CUs” while coding your instructions.

If you have any questions or comments, please reach out and don’t forget to subscribe! Also check out our podcast The Latest Development below!

Reply

or to participate

Keep Reading

No posts found