Rust ownership is the feature most Solana learners struggle with first. Unlike JavaScript or Python, Rust enforces memory safety at compile time through a strict ownership model. Once you understand three rules, most compiler errors become obvious. This guide walks through each rule with concrete Solana examples so the concepts move from abstract to practical.
If you are new to Rust on Solana, start with Rust data types for Solana first, then return here for the ownership model that governs how those types behave in memory.
Section 1
Why Rust ownership exists
Traditional languages trade performance for convenience. Rust refuses to make that trade.
Quick answer
What problem does Rust ownership solve? Ownership eliminates memory bugs (use-after-free, double free, data races) at compile time without a garbage collector.
Traditional languages solve memory management in one of two ways. Garbage collected languages like Python and JavaScript track references at runtime and pause the program periodically to reclaim unused memory. Systems languages like C give you manual malloc and free, which is fast but error prone.
Rust takes a third path. The compiler tracks ownership statically and inserts deallocation code automatically at the exact point where a value goes out of scope. There are no runtime pauses and no manual free calls.
Solana programs run in BPF VMs where every microsecond counts
Rust's ownership system means no surprise GC pauses mid-instruction. The memory management cost is paid entirely at compile time, leaving zero overhead at runtime.
Core concept
Three Rules That Govern Everything
All of Rust's memory behavior follows from three rules. Learn these and most compiler errors become self-explanatory.
Every value has one owner
A value (data in memory) can be owned by exactly one variable at a time. When the variable goes out of scope, the value is dropped.
Ownership can be transferred (moved)
Assigning a value to another variable or passing it to a function transfers ownership. The original variable becomes invalid and the compiler will refuse to compile code that uses it.
When the owner goes out of scope, the value is dropped
Rust automatically calls drop() when the owning variable leaves scope. No manual free(), no garbage collector — the compiler inserts the cleanup code at compile time.
Section 3
Move semantics in practice
Seeing a move error once makes the rule permanent in memory.
Quick answer
What is a move in Rust? A move transfers ownership of a value to a new variable, making the original variable invalid. The compiler rejects any subsequent use of the original.
The following code will not compile. After the assignment on line 2, name no longer owns the String. Attempting to print it on the commented line produces a compile error.
fn main() {
let name = String::from("Alice");
let name2 = name; // ownership MOVES to name2
// println!("{}", name); // COMPILE ERROR: name was moved
println!("{}", name2); // OK
}If you need both variables to remain valid, use .clone() to create a deep copy. This allocates new memory for the second value so both are independent.
let name = String::from("Alice");
let name2 = name.clone(); // deep copy — both are valid
println!("{} and {}", name, name2);Copy vs Clone
Primitive types like u8, u64, bool, and Pubkey implement Copy — they are copied on assignment, not moved. Strings and Vecs do not implement Copy and are moved. In Solana programs, this distinction matters every time you pass a Pubkey or a Vec of bytes to a function.
Section 4
Borrowing with & and &mut
Borrowing lets functions use data without taking ownership. It is the most common pattern in Solana program code.
Quick answer
What is borrowing in Rust? Borrowing lets a function use a value without taking ownership. An immutable borrow (&T) allows reading; a mutable borrow (&mut T) allows writing.
An immutable borrow passes a reference to a function. The function can read the data, but the original owner keeps ownership and the value remains valid after the function returns.
fn print_balance(balance: &u64) {
println!("Balance: {}", balance);
// balance is borrowed — original owner keeps it
}
fn main() {
let balance: u64 = 1_000_000;
print_balance(&balance);
println!("Still accessible: {}", balance); // OK
}A mutable borrow passes a reference that allows the function to modify the value. You must mark both the variable and the call site with mut.
fn add_lamports(balance: &mut u64, amount: u64) {
*balance += amount;
}
fn main() {
let mut balance: u64 = 1_000_000;
add_lamports(&mut balance, 5_000);
println!("New balance: {}", balance); // 1_005_000
}Two rules govern borrows. First, you can have any number of immutable borrows OR exactly one mutable borrow at a time — never both simultaneously. Second, borrows must not outlive the owner.
The borrow checker enforces these rules at compile time
If you try to create a mutable borrow while an immutable borrow exists, the code will not compile. This prevents data races by construction — no locks, no runtime checks, no panics.
Solana pattern
Borrowing in Solana Program Code
AccountInfo uses RefCell to shift the borrow check to runtime, which is required by the Solana account model.
Solana's AccountInfo struct stores account data as a RefCell<&mut [u8]>. RefCell moves the borrow check from compile time to runtime, allowing the Solana runtime to pass the same account to multiple instructions safely. The borrow rules still apply — they are just enforced at runtime with a panic instead of a compile error.
When using the Anchor framework, most account access happens through typed wrappers that handle the borrow for you. But in raw Solana programs, you interact with RefCell directly.
use solana_program::account_info::AccountInfo;
pub fn process(account: &AccountInfo) -> ProgramResult {
// Read account data — immutable borrow
let data = account.data.borrow();
let first_byte = data[0];
drop(data); // explicitly release the borrow
// Write account data — mutable borrow
let mut data = account.data.borrow_mut();
data[0] = first_byte + 1;
Ok(())
}Always drop borrows before calling borrow_mut()
RefCell panics at runtime if you call borrow_mut() while an immutable borrow is still live. Always drop() the immutable borrow first, or let it go out of scope by wrapping the read in a block.
Once you are comfortable with ownership and borrowing, the next step is understanding Rust structs for Solana, which show how these rules apply to the account state types you will define in every program.
Section 6
Frequently asked questions
Common questions about Rust ownership from Solana developers.
What is ownership in Rust?
Ownership is Rust's memory management model. Every value has exactly one owner. When the owner goes out of scope, the value is automatically freed. There is no garbage collector — the compiler handles memory at compile time.
What is the difference between move and borrow in Rust?
A move transfers ownership permanently — the original variable becomes invalid. A borrow (&T or &mut T) is temporary access — the original owner keeps ownership and gets control back when the borrow ends.
Can you have multiple mutable borrows in Rust?
No. Rust allows either any number of immutable borrows (&T) or exactly one mutable borrow (&mut T) at a time, never both. This rule prevents data races at compile time.
Why does Solana use RefCell for account data?
Solana's runtime passes AccountInfo to multiple parts of a program, which requires runtime borrow checking rather than compile-time checking. RefCell defers the borrow check to runtime so Solana can manage account access dynamically while still enforcing the single-writer rule.
Do I need to understand Rust ownership to use Anchor?
Yes, but only the basics. Anchor handles most account lifecycle code for you, but you will still write Rust handlers that borrow account fields. Understanding move vs borrow and the RefCell pattern in AccountInfo saves hours of debugging.