Pick the right collection
Pick NEAR SDK store collections for gas: LookupMap vs IterableMap vs Vector, why a Vec or HashMap in contract state burns gas, and unique storage prefixes.
Intermediate6 min read3-question check
Your contract struct is stored as a single record. On every call near-sdk reads that record and Borsh-deserializes the whole struct; on every change call it serializes and writes it back. Anything you put directly in the struct is paid for on every call, whether the method uses it or not.
The collections in near_sdk::store avoid that: the struct holds only a small handle (a storage prefix, plus a length for the iterable ones), and each entry lives under its own storage key that is read only when you ask for it.
The anti-pattern: native collections in state#
use near_sdk::{near, AccountId};
use std::collections::HashMap;
#[near(contract_state)]
#[derive(Default)]
pub struct Bad {
// 10,000 accounts here means 10,000 entries deserialized
// just to read one balance, and all of them rewritten on every change
balances: HashMap<AccountId, u128>,
}use near_sdk::store::{IterableSet, LookupMap, Vector};
use near_sdk::{near, AccountId, BorshStorageKey};
#[near(serializers = [borsh])]
#[derive(BorshStorageKey)]
enum StorageKey {
Balances,
Members,
History,
}
#[near(contract_state)]
pub struct Good {
balances: LookupMap<AccountId, u128>, // read one key, load one entry
members: IterableSet<AccountId>, // can be listed and paginated
history: Vector<String>, // append-only, read by index
}
impl Default for Good {
fn default() -> Self {
Self {
balances: LookupMap::new(StorageKey::Balances),
members: IterableSet::new(StorageKey::Members),
history: Vector::new(StorageKey::History),
}
}
}Which one to use#
| Collection | Iterable? | What a call loads | Use when |
|---|---|---|---|
LookupMap<K, V> | No | Only the keys you touch | Balances and per-account data looked up by key. The cheapest map. |
LookupSet<T> | No | Only the values you check | Allow-lists, "already claimed" flags |
IterableMap<K, V> | Yes | Keys you touch; iteration walks entries | You must list or paginate entries. Costs extra storage per entry for its key index. |
IterableSet<T> | Yes | Values you touch; iteration walks entries | Membership you must enumerate |
Vector<T> | Yes, by index | Only the indexes you read | Ordered lists, append-only logs, queues |
UnorderedMap / UnorderedSet | Yes | Same as the iterable versions | Legacy: deprecated in near-sdk 5 in favour of IterableMap / IterableSet |
Vec / HashMap in the struct | Yes | Everything, every call | Only small, bounded data such as a handful of admin accounts |
Unique storage prefixes#
Every entry is stored under prefix + key, so two collections with the same prefix read and overwrite each other’s data. A #[derive(BorshStorageKey)] enum gives each collection a distinct prefix that Borsh serializes to a single byte (the variant index), which also keeps every key short.
For nested collections, such as one set of tokens per owner, put the owner into the prefix with a variant that carries data, for example StorageKey::TokensPerOwner { owner_hash: env::sha256(owner.as_bytes()) }. Every inner collection then gets its own key space.
Check yourself
3 questions · progress saved in this browser