Overview
Soroban provides three types of storage with different characteristics and costs. Each type is optimized for specific use cases and has distinct TTL (Time-To-Live) behavior.
Storage Types
Persistent Long-term data that can be restored after expiration
Temporary Short-term data that is permanently deleted after expiration
Instance Contract-level data tied to the contract instance
Accessing Storage
Access storage through the Env:
use soroban_sdk :: { Env , Symbol , symbol_short};
pub fn storage_access ( env : Env ) {
// Persistent storage
let persistent = env . storage () . persistent ();
// Temporary storage
let temporary = env . storage () . temporary ();
// Instance storage
let instance = env . storage () . instance ();
}
Storage can only be accessed within a contract context. Calling storage methods outside a contract will panic with error: “env.as_contract() required”.
Persistent Storage
Characteristics
Durability : Data persists until explicitly deleted or expired
Expiration : Can be restored from Expired State Stack (ESS)
Cost : More expensive than temporary storage
Use Cases : User balances, ownership records, critical state
Basic Operations
use soroban_sdk :: {contracttype, symbol_short};
#[contracttype]
pub enum DataKey {
Balance ( Address ),
TotalSupply ,
}
pub fn persistent_operations ( env : Env , user : Address ) {
let key = DataKey :: Balance ( user );
// Set value
env . storage () . persistent () . set ( & key , & 1000 i128 );
// Get value
let balance : Option < i128 > = env . storage () . persistent () . get ( & key );
// Check existence
let exists = env . storage () . persistent () . has ( & key );
// Remove value
env . storage () . persistent () . remove ( & key );
}
Update Operations
pub fn increment_balance ( env : Env , user : Address , amount : i128 ) {
let key = DataKey :: Balance ( user );
// Update with closure
env . storage () . persistent () . update ( & key , | balance : Option < i128 > | {
balance . unwrap_or ( 0 ) + amount
});
}
pub fn try_update_balance (
env : Env ,
user : Address ,
amount : i128 ,
) -> Result < i128 , Error > {
let key = DataKey :: Balance ( user );
// Update with error handling
env . storage () . persistent () . try_update (
& key ,
| balance : Option < i128 > | {
let current = balance . unwrap_or ( 0 );
if current + amount < 0 {
return Err ( Error :: InsufficientBalance );
}
Ok ( current + amount )
},
)
}
TTL Management
pub fn extend_storage_ttl ( env : Env , key : DataKey ) {
// Extend TTL if below threshold
// threshold: extend only if TTL < 100 ledgers
// extend_to: extend to 1000 ledgers
env . storage () . persistent () . extend_ttl ( & key , 100 , 1000 );
}
pub fn get_max_ttl ( env : Env ) -> u32 {
env . storage () . max_ttl ()
}
Extend TTL before it expires to avoid restoration costs. The extension only happens if current TTL is below the threshold.
Temporary Storage
Characteristics
Durability : Permanently deleted after expiration
Expiration : Cannot be restored
Cost : Cheaper than persistent storage
Use Cases : Caches, oracle data, temporary offers
Basic Operations
pub fn temporary_operations ( env : Env ) {
let key = symbol_short! ( "cache" );
// Set with automatic TTL
env . storage () . temporary () . set ( & key , & 42 u32 );
// Get value
let value : Option < u32 > = env . storage () . temporary () . get ( & key );
// Check and remove
if env . storage () . temporary () . has ( & key ) {
env . storage () . temporary () . remove ( & key );
}
}
TTL Management
pub fn manage_temp_ttl ( env : Env ) {
let key = symbol_short! ( "offer" );
env . storage () . temporary () . set ( & key , & 100 u32 );
// Extend temporary storage TTL
env . storage () . temporary () . extend_ttl ( & key , 10 , 100 );
}
Trying to extend temporary storage beyond max TTL will cause a panic. Always check against env.storage().max_ttl().
Expiration Behavior
#[test]
fn test_temp_expiration () {
let env = Env :: default ();
env . ledger () . set_sequence_number ( 1000 );
env . ledger () . set_min_temp_entry_ttl ( 100 );
let contract = env . register ( Contract , ());
env . as_contract ( & contract , || {
env . storage () . temporary () . set ( & 1 , & 2 );
// After TTL expires, entry is gone forever
env . ledger () . set_sequence_number ( 1100 );
assert! ( ! env . storage () . temporary () . has ( & 1 ));
});
}
Instance Storage
Characteristics
Durability : Same as persistent (can be restored)
Scope : Shared across the contract instance
Loading : Loaded with the contract instance
Size Limit : ~100 KB serialized
Use Cases : Admin address, contract config, token metadata
Basic Operations
pub fn instance_operations ( env : Env , admin : Address ) {
// Set contract admin
env . storage () . instance () . set ( & symbol_short! ( "ADMIN" ), & admin );
// Get admin
let admin : Address = env . storage ()
. instance ()
. get ( & symbol_short! ( "ADMIN" ))
. unwrap ();
// Update config
env . storage () . instance () . update (
& symbol_short! ( "CONFIG" ),
| config : Option < Config > | {
let mut c = config . unwrap_or_default ();
c . fee_rate = 100 ;
c
},
);
}
TTL Management
pub fn extend_instance_ttl ( env : Env ) {
// Extends both instance AND code TTL
env . storage () . instance () . extend_ttl ( 100 , 1000 );
}
Instance storage TTL extension extends both the contract instance and the contract code. This is more efficient than extending them separately.
Storage Patterns
Counter Pattern
#[contracttype]
#[derive( Clone , Default )]
pub struct Counter {
pub count : u32 ,
}
pub fn increment ( env : Env ) -> u32 {
let key = symbol_short! ( "COUNTER" );
let counter = env . storage ()
. persistent ()
. update ( & key , | c : Option < Counter > | {
let mut counter = c . unwrap_or_default ();
counter . count += 1 ;
counter
});
counter . count
}
Registry Pattern
use soroban_sdk :: Map ;
pub fn register_user ( env : Env , user : Address , info : UserInfo ) {
let mut registry : Map < Address , UserInfo > = env . storage ()
. persistent ()
. get ( & symbol_short! ( "REGISTRY" ))
. unwrap_or_else ( || Map :: new ( & env ));
registry . set ( user , info );
env . storage () . persistent () . set ( & symbol_short! ( "REGISTRY" ), & registry );
}
Cache Pattern
pub fn get_cached_price ( env : Env , asset : Symbol ) -> Option < i128 > {
let key = DataKey :: PriceCache ( asset );
// Check cache first
if let Some ( price ) = env . storage () . temporary () . get ( & key ) {
return Some ( price );
}
// Fetch from oracle (expensive operation)
let price = fetch_from_oracle ( & env , asset );
// Cache for 100 ledgers
env . storage () . temporary () . set ( & key , & price );
env . storage () . temporary () . extend_ttl ( & key , 0 , 100 );
Some ( price )
}
Testing Storage
Basic Test
#[cfg(test)]
mod tests {
use super ::* ;
use soroban_sdk :: testutils :: storage :: { Persistent , Instance };
#[test]
fn test_storage () {
let env = Env :: default ();
let contract_id = env . register ( Contract , ());
let client = ContractClient :: new ( & env , & contract_id );
// Set values
client . set_persistent ( & 1 , & 100 );
// Verify
assert_eq! ( client . get_persistent ( & 1 ), Some ( 100 ));
// Check TTL
let ttl = env . as_contract ( & contract_id , || {
env . storage () . persistent () . get_ttl ( & 1 )
});
assert! ( ttl > 0 );
}
}
Testing All Storage Types
#[test]
fn test_all_storage_types () {
let env = Env :: default ();
env . ledger () . set_min_persistent_entry_ttl ( 100 );
env . ledger () . set_min_temp_entry_ttl ( 50 );
let contract = env . register ( Contract , ());
env . as_contract ( & contract , || {
// Test persistent
env . storage () . persistent () . set ( & 1 , & 100 );
assert_eq! ( env . storage () . persistent () . get ( & 1 ), Some ( 100 ));
// Test temporary
env . storage () . temporary () . set ( & 2 , & 200 );
assert_eq! ( env . storage () . temporary () . get ( & 2 ), Some ( 200 ));
// Test instance
env . storage () . instance () . set ( & 3 , & 300 );
assert_eq! ( env . storage () . instance () . get ( & 3 ), Some ( 300 ));
});
}
Viewing All Storage
use soroban_sdk :: testutils :: storage :: { Persistent , Temporary , Instance };
#[test]
fn view_all_storage () {
let env = Env :: default ();
let contract = env . register ( Contract , ());
env . as_contract ( & contract , || {
env . storage () . persistent () . set ( & 1 , & 10 );
env . storage () . persistent () . set ( & 2 , & 20 );
env . storage () . temporary () . set ( & 3 , & 30 );
env . storage () . instance () . set ( & 4 , & 40 );
});
// View all persistent entries
env . as_contract ( & contract , || {
let all_persistent = env . storage () . persistent () . all ();
println! ( "Persistent: {:?}" , all_persistent );
});
}
Storage Costs
Persistent
Temporary
Instance
Write : Moderate cost
Read : Moderate cost
TTL Extension : Low cost
Restoration : Moderate cost
Use when : Data must survive long-term
Write : Low cost
Read : Low cost
TTL Extension : Very low cost
Restoration : Not possible
Use when : Data has limited lifetime
Write : Moderate cost (same as persistent)
Read : Very low cost (loaded with contract)
TTL Extension : Low cost
Size Limit : ~100 KB
Use when : Small, frequently accessed contract config
Best Practices
Choose the right storage type
Use temporary for caches and time-limited data, persistent for critical data, and instance for small contract-level config.
Extend TTL before expiration. Use threshold parameter to avoid unnecessary extensions.
Keep instance storage small
Instance storage is loaded with every contract call. Keep it under 10 KB for optimal performance.
Use Symbol for simple keys, enums with #[contracttype] for complex key structures.
Group related storage operations together to minimize round-trips.
Storage Method Reference
Method Persistent Temporary Instance set(key, val)✓ ✓ ✓ get(key)✓ ✓ ✓ has(key)✓ ✓ ✓ remove(key)✓ ✓ ✓ update(key, fn)✓ ✓ ✓ try_update(key, fn)✓ ✓ ✓ extend_ttl(key, ...)✓ ✓ - extend_ttl(...)- - ✓
Next Steps
Types Learn about types you can store
Authentication Understand auth and authorization