LogoAnchor Docs

Zero Copy

Learn how to use Anchor's zero-copy deserialization feature to handle large account data in Solana programs.

Overview

Zero-copy deserialization allows programs to read and write account data directly from memory without copying or deserializing it. This is essential for handling large accounts efficiently on Solana.

Why Use Zero-Copy?

Traditional account deserialization (Account<T>) copies data from the account into a heap-allocated struct. This has limitations:

  • Size Constraints: Stack (4KB) and heap (32KB) limits restrict account sizes
  • Compute Cost: Deserialization consumes significant compute units
  • Memory Overhead: Data is duplicated in memory

Zero-copy (AccountLoader<T>) instead:

  • Direct Access: Casts raw account bytes to the struct type (no copying)
  • Larger Accounts: Supports accounts up to 10MB (10,485,760 bytes)
  • Lower Compute: ~90% reduction in CU usage for large accounts
  • In-Place Updates: Modifies account data directly

Performance Comparison

Account SizeAccount<T>AccountLoader<T>Improvement
1 KB~8,000 CU~1,500 CU81% faster
10 KB~50,000 CU~5,000 CU90% faster
100 KBToo large~12,000 CUPossible
1 MBImpossible~25,000 CUPossible

When to Use Zero-Copy

Use zero-copy for:

  • Accounts larger than 1KB
  • Arrays with many elements (orderbooks, event queues)
  • High-frequency operations
  • Compute-sensitive programs Use regular Account<T> for:
  • Small accounts (< 1KB)
  • Dynamic data structures (Vec, String, HashMap)
  • Frequently changing schemas
  • Simple state that doesn't need optimization

Usage

Zero copy is a deserialization feature that allows programs to read account data directly from memory without copying it. This is particularly useful when working with large accounts.

To use zero-copy add the bytemuck crate to your dependencies. Add the min_const_generics feature to allow working with arrays of any size in your zero-copy types.

Cargo.toml
[dependencies]
bytemuck = { version = "1.20.0", features = ["min_const_generics"] }
anchor-lang = "0.32.1"

Define a Zero Copy Account

To define an account type that uses zero-copy, annotate the struct with #[account(zero_copy)].


#[account(zero_copy)]
pub struct Data {
    // 10240 bytes - 8 bytes account discriminator
    pub data: [u8; 10232],
}

The #[account(zero_copy)] attribute automatically implements several traits required for zero-copy deserialization:


#[derive(Copy, Clone)]
#[derive(bytemuck::Zeroable)]
#[derive(bytemuck::Pod)]
#[repr(C)]
struct Data {
  // --snip--
}

Use AccountLoader for Zero Copy Accounts

To deserialize a zero-copy account, use AccountLoader<'info, T>, where T is the zero-copy account type defined with the #[account(zero_copy)] attribute.

For example:

#[derive(Accounts)]
pub struct InstructionAccounts<'info> {


    pub zero_copy_account: AccountLoader<'info, Data>,
}

Initialize a Zero Copy Account

The init constraint can be used with the AccountLoader type to create a zero-copy account.

#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(


        init,
        // 10240 bytes is max space to allocate with init constraint
        space = 8 + 10232,
        payer = payer,
    )]
    pub data_account: AccountLoader<'info, Data>,
    #[account(mut)]
    pub payer: Signer<'info>,
    pub system_program: Program<'info, System>,
}

The init constraint is limited to allocating a maximum of 10240 bytes due to CPI limitations. Under the hood, the init constraint makes a CPI call to the SystemProgram to create the account.

When initializing a zero-copy account for the first time, use load_init to get a mutable reference to the account data. The load_init method also sets the account discriminator.

pub fn initialize(ctx: Context<Initialize>) -> Result<()> {


    let account = &mut ctx.accounts.data_account.load_init()?;
    account.data = [1; 10232];
    Ok(())
}

For accounts that require more than 10240 bytes, use the zero constraint instead of init. The zero constraint verifies the account has not been initialized by checking that its discriminator has not been set.

#[derive(Accounts)]
pub struct Initialize<'info> {


    #[account(zero)]
    pub data_account: AccountLoader<'info, Data>,
}

With the zero constraint, you'll need to first create the account in a separate instruction by directly calling the System Program. This allows you to create accounts up to Solana's maximum account size of 10MB (10_485_760 bytes), bypassing the CPI limitation.

Just as before, use load_init to get a mutable reference to the account data and set the account discriminator. Since 8 bytes are reserved for the account discriminator, the maximum data size is 10_485_752 bytes (10MB - 8 bytes).

pub fn initialize(ctx: Context<Initialize>) -> Result<()> {


    let account = &mut ctx.accounts.data_account.load_init()?;
    account.data = [1; 10_485_752];
    Ok(())
}

Update a Zero Copy Account

Use load_mut when you need mutable access to update an existing zero-copy account:

#[derive(Accounts)]
pub struct Update<'info> {

    #[account(mut)]
    pub data_account: AccountLoader<'info, Data>,
}
pub fn update(ctx: Context<Update>) -> Result<()> {


    let account = &mut ctx.accounts.data_account.load_mut()?;
    account.data = [2; 10232];
    Ok(())
}

Read a Zero Copy Account

Use load to only read the account data.

#[derive(Accounts)]
pub struct ReadOnly<'info> {
    pub data_account: AccountLoader<'info, Data>,
}
pub fn read_only(ctx: Context<ReadOnly>) -> Result<()> {


    let account = &ctx.accounts.data_account.load()?;
    msg!("First 10 bytes: {:?}", &account.data[..10]);
    Ok(())
}

Common Patterns

Nested Zero-Copy Types

For types used within zero-copy accounts, use #[zero_copy] (without account):

#[account(zero_copy)]
pub struct OrderBook {
    pub market: Pubkey,
    pub bids: [Order; 1000],
    pub asks: [Order; 1000],
}
 
//]
#[zero_copy]
pub struct Order {
    pub trader: Pubkey,
    pub price: u64,
    pub quantity: u64,
}

Accessor Methods for Byte Arrays

Zero-copy uses #[repr(packed)], making field references unsafe. Use the #[accessor] attribute for safe getter/setter methods:

#[account(zero_copy)]
pub struct Config {
    pub authority: Pubkey,

    #[accessor(Pubkey)]
    pub secondary_authority: [u8; 32],
}
 
// Usage:
let config = &mut ctx.accounts.config.load_mut()?;
let secondary = config.get_secondary_authority();
config.set_secondary_authority(&new_authority);

Zero-Copy with PDAs

Zero-copy accounts work seamlessly with program-derived addresses:

#[derive(Accounts)]
pub struct CreatePdaAccount<'info> {
    #[account(
        init,

        seeds = [b"data", authority.key().as_ref()],
        bump,
        payer = authority,
        space = 8 + std::mem::size_of::<Data>(),
    )]
    pub data_account: AccountLoader<'info, Data>,
    #[account(mut)]
    pub authority: Signer<'info>,
    pub system_program: Program<'info, System>,
}

Separate Types for RPC Parameters

Zero-copy types cannot derive AnchorSerialize/AnchorDeserialize. Use separate types for instruction parameters:

// For zero-copy account
#[zero_copy]
pub struct Event {
    pub from: Pubkey,
    pub data: u64,
}
 
// For RPC/instruction parameters

#[derive(AnchorSerialize, AnchorDeserialize)]
pub struct EventParams {
    pub from: Pubkey,
    pub data: u64,
}
 
impl From<EventParams> for Event {
    fn from(params: EventParams) -> Self {
        Event {
            from: params.from,
            data: params.data,
        }
    }
}

Common Pitfalls

Forgetting the Account Discriminator

Always add 8 bytes for the account discriminator when calculating space:

// Wrong - missing discriminator
space = std::mem::size_of::<Data>()
 
//    Correct - includes discriminator

space = 8 + std::mem::size_of::<Data>()

Using Dynamic Types

Zero-copy requires all fields to be Copy types:

#[account(zero_copy)]
pub struct InvalidData {
    pub items: Vec<u64>,  // Vec is not Copy
    pub name: String,     // String is not Copy
}
 
#[account(zero_copy)]
pub struct ValidData {
    pub items: [u64; 100], //     Fixed-size array
    pub name: [u8; 32],    //     Fixed-size bytes
}

Using load_init vs load_mut

Use load_init() for first-time initialization (sets discriminator):

// First initialization
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {

    let account = &mut ctx.accounts.data_account.load_init()?;
    account.data = [1; 10232];
    Ok(())
}
 
// Subsequent updates
pub fn update(ctx: Context<Update>) -> Result<()> {

    let account = &mut ctx.accounts.data_account.load_mut()?;
    account.data = [2; 10232];
    Ok(())
}

Not Validating Array Indices

Always validate array indices to prevent panics:

pub fn update_item(
    ctx: Context<Update>,
    index: u32,
    value: u64
) -> Result<()> {
    let account = &mut ctx.accounts.data_account.load_mut()?;
    

    require!(
        (index as usize) < account.items.len(),
        ErrorCode::IndexOutOfBounds
    );
    
    account.items[index as usize] = value;
    Ok(())
}

Real-World Use Cases

Event Queue Pattern

Store large sequences of events efficiently:

#[account(zero_copy)]
pub struct EventQueue {
    pub head: u64,
    pub count: u64,
    pub events: [Event; 10000],
}
 
#[zero_copy]
pub struct Event {
    pub timestamp: i64,
    pub user: Pubkey,
    pub event_type: u8,
    pub data: [u8; 32],
}

Used by: Trading protocols, audit logs, messaging systems

Order Book Pattern

Efficient storage for trading pairs:

#[account(zero_copy)]
pub struct OrderBook {
    pub market: Pubkey,
    pub bid_count: u32,
    pub ask_count: u32,
    pub bids: [Order; 1000],
    pub asks: [Order; 1000],
}
 
#[zero_copy]
pub struct Order {
    pub trader: Pubkey,
    pub price: u64,
    pub size: u64,
    pub timestamp: i64,
}

Used by: DEXs (Serum, Mango), NFT marketplaces

Examples

The examples below demonstrate two approaches for initializing zero-copy accounts in Anchor:

  1. Using the init constraint to initialize the account in a single instruction
  2. Using the zero constraint to initialize an account with data greater than 10240 bytes

Zero Copy

lib.rs
use anchor_lang::prelude::*;
 
declare_id!("8B7XpDXjPWodpDUWDSzv4q9k73jB5WdNQXZxNBj1hqw1");
 
#[program]
pub mod zero_copy {
    use super::*;
    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
        let account = &mut ctx.accounts.data_account.load_init()?;
        account.data = [1; 10232];
        Ok(())
    }
 
    pub fn update(ctx: Context<Update>) -> Result<()> {
        let account = &mut ctx.accounts.data_account.load_mut()?;
        account.data = [2; 10232];
        Ok(())
    }
}
 
#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(
        init,
        // 10240 bytes is max space to allocate with init constraint
        space = 8 + 10232,
        payer = payer,
    )]
    pub data_account: AccountLoader<'info, Data>,
    #[account(mut)]
    pub payer: Signer<'info>,
    pub system_program: Program<'info, System>,
}
 
#[derive(Accounts)]
pub struct Update<'info> {
    #[account(mut)]
    pub data_account: AccountLoader<'info, Data>,
}
 
#[account(zero_copy)]
pub struct Data {
    // 10240 bytes - 8 bytes account discriminator
    pub data: [u8; 10232],
}

Initialize Large Account

When initializing an account that requires more than 10,240 bytes of space, you must split the initialization into two steps:

  1. Create the account in a separate instruction invoking the System Program
  2. Initialize the account data in your program instruction

Note that the maximum Solana account size is 10MB (10_485_760 bytes), 8 bytes are reserved for the account discriminator.

lib.rs
use anchor_lang::prelude::*;
 
declare_id!("CZgWhy3FYPFgKE5v9atSGaiQzbSB7cM38ofwX1XxeCFH");
 
#[program]
pub mod zero_copy_two {
    use super::*;
    pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
        let account = &mut ctx.accounts.data_account.load_init()?;
        account.data = [1; 10_485_752];
        Ok(())
    }
}
 
#[derive(Accounts)]
pub struct Initialize<'info> {
    #[account(zero)]
    pub data_account: AccountLoader<'info, Data>,
}
 
#[account(zero_copy)]
pub struct Data {
    // 10240 bytes - 8 bytes account discriminator
    pub data: [u8; 10_485_752],
}