Anchor vs Pinocchio vs Steel: three Solana program frameworks, compared
Anchor for ergonomics, Pinocchio for performance, Steel for somewhere in between. How the three Solana program frameworks compare, and when to pick each.
For most of Solana's history, "how do I write a Solana program" had two answers: raw Rust on top of solana-program, or Anchor. Anchor won, hard — at one point ~85% of Solana programs used it. In 2024-2026, two new options reset the conversation: Pinocchio and Steel. The frameworks now span a clear spectrum: max ergonomics, max performance, and the middle path.
Anchor — ergonomic, batteries-included
Anchor wraps the raw solana-program surface with derive macros for account validation, IDL generation, an opinionated directory layout, and a TypeScript client that consumes the IDL.
use anchor_lang::prelude::*;
#[program]
pub mod vault {
use super::*;
pub fn deposit(ctx: Context<Deposit>, amount: u64) -> Result<()> {
ctx.accounts.vault.balance += amount;
Ok(())
}
}
#[derive(Accounts)]
pub struct Deposit<'info> {
#[account(mut, has_one = owner)]
pub vault: Account<'info, Vault>,
pub owner: Signer<'info>,
}That #[account(mut, has_one = owner)] is doing serious work: deserialization, ownership check, mutability marking. Anchor generates the verification code so you can't accidentally forget.
Trade-offs: Anchor adds ~5-15k CUs of overhead per instruction (deserialization, account validation, error wrapping). Binary size is larger. For 95% of programs, this overhead is invisible. For DEXes, perp engines, or anything operating near the 1.4M CU per-tx ceiling, it's real.
Pinocchio — zero-copy, minimal-allocation
Pinocchio is a no-std, no-alloc framework written by Anza. It avoids the standard solana-program dependencies entirely and works directly with the program entrypoint's raw account data — no deserialization unless you ask for it.
use pinocchio::{
account_info::AccountInfo,
entrypoint,
pubkey::Pubkey,
ProgramResult,
};
entrypoint!(process_instruction);
pub fn process_instruction(
_program_id: &Pubkey,
accounts: &[AccountInfo],
instruction_data: &[u8],
) -> ProgramResult {
let [vault_info, owner_info, _rest @ ..] = accounts else {
return Err(ProgramError::NotEnoughAccountKeys);
};
// Read amount directly from instruction_data — no deserialization layer
let amount = u64::from_le_bytes(instruction_data[..8].try_into().unwrap());
// Mutate vault data in-place
let mut data = vault_info.try_borrow_mut_data()?;
let balance = u64::from_le_bytes(data[..8].try_into().unwrap()) + amount;
data[..8].copy_from_slice(&balance.to_le_bytes());
Ok(())
}That program might use 500-2000 CUs total. The same logic in Anchor would be 5-15x more. The catch: you write all the validation yourself. No has_one macro, no automatic ownership checks. One forgotten check = exploit.
Used heavily in the SPL programs Anza rewrote in 2024-2025 (the new p-token, Token-2022 internals, others) where every CU matters.
Steel — the middle path
Steel is a lighter-weight Anchor alternative built by Regolith Labs. It keeps the account-validation ergonomics (derive macros, typed accounts) but throws out the heavy parts (IDL generation, the bundled client framework, the full solana-program dependency tree).
use steel::*;
#[repr(u8)]
#[derive(Clone, Copy, ShankInstruction)]
pub enum VaultInstruction {
Deposit = 0,
}
instruction!(VaultInstruction, Deposit);
#[repr(C)]
#[derive(Clone, Copy, Pod, Zeroable, AccountValidator)]
pub struct Deposit {
pub amount: u64,
}
pub fn process_deposit(accounts: &[AccountInfo], data: &[u8]) -> ProgramResult {
let args = Deposit::try_from_bytes(data)?;
let [vault, owner] = accounts[..2] else { return Err(ProgramError::NotEnoughAccountKeys) };
owner.is_signer()?;
vault.has_owner(&crate::ID)?;
vault.is_writable()?;
let mut vault_data = vault.as_account_mut::<Vault>()?;
vault_data.balance += args.amount;
Ok(())
}Steel programs typically come in 2-3x cheaper than Anchor on CUs, with most of the safety properties intact. Validation is explicit but compact (is_signer(), has_owner()).
The comparison
Order-of-magnitude numbers for a typical "deposit to vault" instruction:
- Anchor: ~12,000 CUs, ~600KB binary, full IDL, generated TS client
- Steel: ~4,000 CUs, ~150KB binary, Shank IDL available, no bundled client
- Pinocchio: ~800 CUs, ~30KB binary, no IDL by default, no client
Picking one
Use Anchor when: you're building a normal app program, your team includes engineers new to Solana, you want the biggest ecosystem of examples to copy from. This is still the right default for most projects.
Use Steel when: you care about CUs but not at the Pinocchio level. You want the validation ergonomics, you don't want the runtime overhead. Good for second-iteration programs where you've identified bottlenecks.
Use Pinocchio when: you're writing infrastructure (token programs, oracles, AMM cores) where every CU matters, the program shape is stable, and you can afford to hand-write validation.
What to actually do
Anchor for the first version of a new program. If CU usage becomes a constraint, port the hot path(s) to Steel or Pinocchio. Treat the choice as "tier of optimisation," not religion — many production protocols ship Anchor + a Pinocchio-rewritten inner loop called via CPI.