Rust for Smart Contracts & Performance-Sensitive Systems: Patterns from Production

·5 min read

Rust for Smart Contracts & Performance-Sensitive Systems

Rust shows up in two very different places on a serious Web3 project: on-chain (Solana programs, and increasingly EVM precompile-adjacent tooling) and off-chain (the trading backends, ingesters, and game engines that talk to those contracts). Both benefit from the same things - memory safety, predictable performance, strong types - but the patterns that matter are different.

This post is the shortlist I actually reach for when writing Rust for smart contracts and for the performance-sensitive systems that drive them.

Why Rust, honestly

Marketing says "memory safety and zero-cost abstractions". In production I care about three things:

  1. Predictable latency. No GC pauses and tight control over allocations means p99 is close to p50. That is the difference between "lands the bundle" and "misses the slot".
  2. Types that make illegal states unrepresentable. An enum with exhaustive matching will catch more bugs than any test suite.
  3. The compiler is the first reviewer. On a smart contract, a data race or double-free is not a segfault - it is a lost treasury. Rust rejects entire classes of those before you run cargo test.

On-chain Rust: Solana programs

On Solana, Rust is the language. Anchor is the framework. The patterns that keep a program safe and cheap:

1. Account model first, code second

You design accounts before you write instructions. Which PDA seeds? Which authority? What's owned by the program? Skipping this step is how you end up with remaining_accounts spaghetti and CPIs that surprise you.

#[account]
pub struct MarketState {
    pub authority: Pubkey,
    pub bump: u8,
    pub total_volume: u64,
    pub fee_bps: u16,
    pub paused: bool,
}
 
#[derive(Accounts)]
pub struct PlaceTrade<'info> {
    #[account(
        mut,
        seeds = [b"market", market.key().as_ref()],
        bump = market_state.bump,
        constraint = !market_state.paused @ ErrorCode::MarketPaused,
    )]
    pub market_state: Account<'info, MarketState>,
    #[account(mut)]
    pub trader: Signer<'info>,
    // ...
}

constraint = ... @ ErrorCode::X is the pattern I use everywhere - the account validation and the error message live in the same place, which is much easier to audit than if-statements scattered through the instruction body.

2. Compute-unit budget is a feature

You have ~200k CU to spend per instruction (1.4M per tx). Every CPI, every Copy-style clone, every large serialization costs budget. Two habits:

#[account(zero_copy)]
#[repr(C)]
pub struct Orderbook {
    pub bids: [Order; 1024],
    pub asks: [Order; 1024],
}

3. Checked arithmetic, always

let new_balance = user
    .balance
    .checked_add(amount)
    .ok_or(ErrorCode::Overflow)?;

Unchecked arithmetic + user-controlled inputs is a recurring pattern in exploit reports. Anchor's default panics are safer than EVM-style silent wrapping, but explicit checks read better to auditors and are cheap.

4. CPIs with signer seeds, not hope

When the program signs on behalf of a PDA (SPL-Token transfers from a vault, re-distributing fees, etc.), use the signer-seeds form:

let seeds = &[b"vault", market.key().as_ref(), &[vault_bump]];
let signer_seeds = &[&seeds[..]];
 
token::transfer(
    CpiContext::new_with_signer(
        token_program.to_account_info(),
        Transfer {
            from: vault.to_account_info(),
            to:   recipient.to_account_info(),
            authority: vault_authority.to_account_info(),
        },
        signer_seeds,
    ),
    amount,
)?;

Don't reinvent signing with invoke_signed by hand unless you have to.

Off-chain Rust: performance-sensitive backends

Off-chain, Rust earns its keep in the services that have to react in milliseconds: ingesters, routers, bundlers, game engines, market makers. The pattern that keeps those systems honest:

1. Tokio is the runtime, not a toolbox

Pick a structure early:

let (tx, mut rx) = tokio::sync::mpsc::channel::<Update>(1024);
 
tokio::spawn(async move {
    while let Some(update) = rx.recv().await {
        if let Err(e) = strategy.on_update(update).await {
            tracing::error!(?e, "strategy error");
        }
    }
});

Channels with bounded capacity are non-negotiable. An unbounded channel hides backpressure until OOM.

2. Zero-copy parsing on the hot path

Whenever you're decoding gRPC/geyser messages at thousands of events per second, avoid .clone() and String::from(...) in the decode path. Use Bytes, &[u8], and borrowed data all the way to the strategy.

3. Async traits without tears

#[async_trait::async_trait]
pub trait Executor: Send + Sync {
    async fn submit(&self, tx: PreparedTx) -> Result<Signature, ExecError>;
}

Mock the trait in tests. Swap Jito / Nozomi / direct-RPC implementations at runtime via config. This is how you A/B-test inclusion providers without rewriting the strategy.

4. Error types that say what happened

#[derive(thiserror::Error, Debug)]
pub enum ExecError {
    #[error("jito bundle rejected: {0}")]
    JitoRejected(String),
    #[error("rpc error: {0}")]
    Rpc(#[from] solana_client::client_error::ClientError),
    #[error("risk rejected: {0:?}")]
    Risk(crate::risk::RiskError),
}

anyhow everywhere is tempting. In code you'll actually have to alert on, thiserror variants pay for themselves - you can pattern-match in the metrics layer and count specific failures.

5. Instrumentation is not optional

use tracing::{info, instrument};
 
#[instrument(skip(ctx), fields(strategy = %self.name))]
pub async fn on_signal(&self, ctx: &Ctx, signal: Signal) -> anyhow::Result<()> {
    info!(?signal, "signal received");
    // ...
    Ok(())
}

Structured tracing spans + a metrics crate (metrics, prometheus) are the lowest-effort, highest-leverage instrumentation I know.

Shared patterns: where on-chain meets off-chain

The most common mistake I see in Web3 Rust codebases: repeating types between on-chain and off-chain. Every discriminator, every account struct, every enum drifts.

Fix:

programs/my_market        # on-chain, #[program]
client/my-market-client   # re-exports types + pure helpers
bots/my-market-arb        # depends on client

When you change an account layout, the bot refuses to compile. That's what you want.

Security checklist for Rust smart contracts

A compressed version of what I run through before a mainnet deploy:

Performance checklist for off-chain Rust

Conclusion

Rust earns its reputation in Web3 for the same reason it earns it in kernels and browsers: when you need the system to be both safe and fast, almost nothing else gets you there with so little ceremony.

The trick is to stop treating "on-chain Rust" and "off-chain Rust" as different languages. The patterns above - typed state, bounded channels, checked arithmetic, signer-seed CPIs, instrumented async - compose. You use them in the smart contract, you use them in the bundler, and the whole system stays honest.

Related posts