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 Anchor accounts (Account<T>) copy data from the account into a heap-allocated struct during deserialization. This has significant limitations:
Limitations:
Stack Limit : 4KB maximum
Heap Limit : 32KB maximum
Compute Cost : Deserialization consumes compute units proportional to data size
Memory Overhead : Data is duplicated in memory
Zero-Copy Benefits:
Direct Access : Casts raw account bytes to 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
Account Size Account<T> AccountLoader<T> Improvement 1 KB ~8,000 CU ~1,500 CU 81% faster 10 KB ~50,000 CU ~5,000 CU 90% faster 100 KB Too large ~12,000 CU Possible 1 MB Impossible ~25,000 CU Possible
When to Use Zero-Copy
Use Zero-Copy For
Accounts larger than 1KB
Arrays with many elements (orderbooks, event queues)
High-frequency read/write operations
Compute-sensitive programs
Fixed-size data structures
Use Regular Account<T> For
Small accounts (< 1KB)
Dynamic data structures (Vec, String, HashMap)
Frequently changing schemas
Simple state that doesn’t need optimization
Basic Usage
1. Add Bytemuck Dependency
Add bytemuck to enable zero-copy features:
[ dependencies ]
anchor-lang = "0.32.1"
bytemuck = { version = "1.20.0" , features = [ "min_const_generics" ] }
The min_const_generics feature allows working with arrays of any size.
2. Define a Zero-Copy Account
Use the #[account(zero_copy)] attribute:
use anchor_lang :: prelude ::* ;
#[account(zero_copy)]
pub struct Data {
pub data : [ u8 ; 10232 ], // 10240 bytes - 8 byte discriminator
}
The attribute automatically implements required traits:
Copy - Allows bitwise copying
Clone - Enables cloning
bytemuck::Zeroable - Allows creation from zeroed bytes
bytemuck::Pod - “Plain Old Data” marker
#[repr(C)] - C-compatible memory layout
3. Use AccountLoader
Replace Account<'info, T> with AccountLoader<'info, T>:
#[derive( Accounts )]
pub struct ProcessData <' info > {
#[account( mut )]
pub data_account : AccountLoader <' info , Data >,
}
Account Operations
Initialize (Small Accounts ≤ 10240 bytes)
For accounts up to 10,240 bytes, use the init constraint:
#[derive( Accounts )]
pub struct Initialize <' info > {
#[account(
init,
payer = payer,
space = 8 + 10232, // 8 bytes discriminator + data
)]
pub data_account : AccountLoader <' info , Data >,
#[account( mut )]
pub payer : Signer <' info >,
pub system_program : Program <' info , System >,
}
pub fn initialize ( ctx : Context < Initialize >) -> Result <()> {
let data = & mut ctx . accounts . data_account . load_init () ? ;
data . data = [ 1 ; 10232 ];
Ok (())
}
The init constraint is limited to 10,240 bytes due to CPI limitations when calling the System Program.
Initialize (Large Accounts > 10240 bytes)
For accounts larger than 10,240 bytes, use the zero constraint and create the account separately:
Program:
#[account(zero_copy)]
pub struct LargeData {
pub data : [ u8 ; 10_485_752 ], // 10MB - 8 bytes discriminator
}
#[derive( Accounts )]
pub struct InitializeLarge <' info > {
#[account(zero)] // Verifies discriminator not set
pub data_account : AccountLoader <' info , LargeData >,
}
pub fn initialize_large ( ctx : Context < InitializeLarge >) -> Result <()> {
let data = & mut ctx . accounts . data_account . load_init () ? ;
data . data = [ 1 ; 10_485_752 ];
Ok (())
}
Client (create account first):
import * as anchor from "@anchor-lang/core" ;
const dataAccount = anchor . web3 . Keypair . generate ();
const space = 10_485_760 ; // 10MB max account size
const lamports = await program . provider . connection
. getMinimumBalanceForRentExemption ( space );
// Create account via System Program
const createAccountIx = anchor . web3 . SystemProgram . createAccount ({
fromPubkey: program . provider . publicKey ,
newAccountPubkey: dataAccount . publicKey ,
space ,
lamports ,
programId: program . programId ,
});
// Initialize account in your program
const initializeIx = await program . methods
. initializeLarge ()
. accounts ({ dataAccount: dataAccount . publicKey })
. instruction ();
// Send both instructions
const tx = new anchor . web3 . Transaction ()
. add ( createAccountIx )
. add ( initializeIx );
await program . provider . sendAndConfirm ( tx , [ dataAccount ]);
Read Data
Use load() for read-only access:
#[derive( Accounts )]
pub struct ReadData <' info > {
pub data_account : AccountLoader <' info , Data >,
}
pub fn read_data ( ctx : Context < ReadData >) -> Result <()> {
let data = ctx . accounts . data_account . load () ? ;
msg! ( "First 10 bytes: {:?}" , & data . data[ .. 10 ]);
Ok (())
}
Update Data
Use load_mut() for mutable access:
#[derive( Accounts )]
pub struct UpdateData <' info > {
#[account( mut )]
pub data_account : AccountLoader <' info , Data >,
}
pub fn update_data ( ctx : Context < UpdateData >) -> Result <()> {
let data = & mut ctx . accounts . data_account . load_mut () ? ;
data . data = [ 2 ; 10232 ];
Ok (())
}
Advanced Patterns
Nested Zero-Copy Types
Define reusable zero-copy types with #[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
Since zero-copy uses #[repr(packed)], field references are unsafe. Use #[accessor] for safe getters/setters:
#[account(zero_copy)]
pub struct Config {
pub authority : Pubkey ,
#[accessor( Pubkey )] // Generate safe accessor methods
pub secondary_authority : [ u8 ; 32 ],
}
// Usage:
pub fn use_accessors ( ctx : Context < UseConfig >) -> Result <()> {
let config = & mut ctx . accounts . config . load_mut () ? ;
// Safe getter
let secondary = config . get_secondary_authority ();
msg! ( "Secondary authority: {}" , secondary );
// Safe setter
let new_authority = Pubkey :: new_unique ();
config . set_secondary_authority ( & new_authority );
Ok (())
}
Zero-Copy with PDAs
Zero-copy accounts work seamlessly with PDAs:
#[derive( Accounts )]
pub struct InitializePDA <' info > {
#[account(
init,
payer = authority,
space = 8 + std :: mem :: size_of :: < Data >(),
seeds = [ b"data" , authority . key() . as_ref()],
bump ,
)]
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:
// Zero-copy account
#[zero_copy]
pub struct Event {
pub from : Pubkey ,
pub data : u64 ,
}
// RPC parameter type
#[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,
}
}
}
pub fn create_event (
ctx : Context < CreateEvent >,
params : EventParams ,
) -> Result <()> {
let event = & mut ctx . accounts . event . load_init () ? ;
* event = params . into ();
Ok (())
}
Common Pitfalls
1. Forgetting the Account Discriminator
Always add 8 bytes for the account discriminator:
// Wrong - missing discriminator
space = std :: mem :: size_of :: < Data >()
// Correct - includes discriminator
space = 8 + std :: mem :: size_of :: < Data >()
2. Using Dynamic Types
Zero-copy requires all fields to be Copy types:
// Invalid - Vec is not Copy
#[account(zero_copy)]
pub struct InvalidData {
pub items : Vec < u64 >, // Error!
pub name : String , // Error!
}
// Valid - Fixed-size arrays
#[account(zero_copy)]
pub struct ValidData {
pub items : [ u64 ; 100 ],
pub name : [ u8 ; 32 ],
}
3. Using load_init vs load_mut
Use load_init() only for first-time initialization :
// First initialization - sets discriminator
pub fn initialize ( ctx : Context < Initialize >) -> Result <()> {
let data = & mut ctx . accounts . data_account . load_init () ? ;
data . data = [ 1 ; 10232 ];
Ok (())
}
// Subsequent updates
pub fn update ( ctx : Context < Update >) -> Result <()> {
let data = & mut ctx . accounts . data_account . load_mut () ? ;
data . data = [ 2 ; 10232 ];
Ok (())
}
4. Not Validating Array Indices
Always validate array indices to prevent panics:
pub fn update_item (
ctx : Context < Update >,
index : u32 ,
value : u64 ,
) -> Result <()> {
let data = & mut ctx . accounts . data_account . load_mut () ? ;
require! (
( index as usize ) < data . items . len (),
ErrorCode :: IndexOutOfBounds
);
data . 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
Ring Buffer Pattern
Circular buffer for fixed-size history:
#[account(zero_copy)]
pub struct RingBuffer {
pub head : u64 ,
pub tail : u64 ,
pub capacity : u64 ,
pub items : [ Item ; 1000 ],
}
pub fn push_item (
ctx : Context < PushItem >,
item : Item ,
) -> Result <()> {
let buffer = & mut ctx . accounts . buffer . load_mut () ? ;
let index = buffer . head % buffer . capacity;
buffer . items[ index as usize ] = item ;
buffer . head = buffer . head . wrapping_add ( 1 );
Ok (())
}
Best Practices
Calculate Space Requirements Accurately
Use std::mem::size_of to calculate exact sizes: space = 8 + std :: mem :: size_of :: < Data >()
Document Size Constraints
Comment size calculations for clarity: #[account(zero_copy)]
pub struct Data {
// Total: 10240 bytes
pub header : [ u8 ; 32 ], // 32 bytes
pub items : [ Item ; 100 ], // 100 * 100 = 10000 bytes
pub footer : [ u8 ; 208 ], // 208 bytes
} // Total: 32 + 10000 + 208 = 10240
Use Type Aliases for Large Arrays
Make code more readable: type OrderArray = [ Order ; 1000 ];
#[account(zero_copy)]
pub struct OrderBook {
pub bids : OrderArray ,
pub asks : OrderArray ,
}
Test with large accounts to verify compute budget: #[test]
fn test_large_account () {
// Test with maximum data size
let data = Data {
data : [ 255 ; 10_485_752 ],
};
// Verify operations succeed
}
Resources