Near Learn

Shrink your state, byte by byte

Shrink NEAR contract state to cut storage staking: Borsh sizes, short prefixes, numbers instead of strings, u64 vs u128, and per-entry byte math in NEAR.

Intermediate5 min read3-question check

Every stored byte keeps 10¹⁹ yoctoNEAR locked, and whatever you store per entry is multiplied by the number of entries. A few bytes saved on a record that exists once per user is real money at scale, and it also means less data to read and write on every call.

What one entry really costs#

The protocol counts each storage record as key length + value length + 40 bytes of fixed overhead (the num_extra_bytes_record runtime parameter). For a store::LookupMap the key is the collection prefix followed by the Borsh-encoded key.

Worked example: a LookupMap<AccountId, u128> with a 1-byte prefix and the key alice.near takes 1 + (4 + 10) bytes of key, 16 bytes of value and 40 bytes of overhead, 71 bytes in total. That is 71 × 10¹⁹ yoctoNEAR, about 0.00071 NEAR. A million such accounts lock roughly 710 NEAR; with 64-character implicit account IDs each entry grows to 125 bytes.

TypeBytes
bool, u81
u324
u648
u128 (token amounts)16
String, AccountId4-byte length + UTF-8 bytes
Vec<T>4-byte length + each item
[u8; 32] (e.g. a hash)32, no length prefix
Option<T>1 tag byte, plus T when Some
enum1 tag byte + the variant’s fields
Borsh encoded sizes

Habits that save bytes#

  • Store numbers as numbers. One NEAR in yoctoNEAR as a u128 is 16 bytes; as the decimal string "1000000000000000000000000" it is 4 + 25 = 29 bytes. Use U128 only at the JSON boundary.
  • Right-size integers. Counters and indexes rarely need more than u32; nanosecond timestamps fit in u64. Reserve u128 for token amounts.
  • Keep prefixes short. The prefix is repeated in every key: a one-byte enum prefix beats b"user_balances_v2" on every single entry.
  • Do not repeat the key in the value. A map keyed by AccountId does not need the account ID inside the value struct as well.
  • Group fields that are read together. Three maps keyed by the same account pay the key and the 40-byte overhead three times; one map of a small struct pays them once.
  • Use enums for states. A status enum is 1 byte; the string "pending" is 11.
  • Do not store what you can derive cheaply in the same call, and keep large blobs (images, long text) off-chain, storing a 32-byte hash or a short URL instead.
Packing a record: EVM slots vs Borsh bytes
Solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

contract Club {
    struct Member {
        uint64 joinedAt; // these three fields are packed
        uint32 points;   // into a single 32-byte slot
        bool active;
    }

    mapping(address => Member) public members;
}
Rust
use near_sdk::store::LookupMap;
use near_sdk::{near, AccountId};

#[near(serializers = [borsh])]
pub struct Member {
    joined_at: u64, // 8 bytes
    points: u32,    // 4 bytes
    active: bool,   // 1 byte
} // 13 bytes, no padding

#[near(contract_state)]
pub struct Club {
    members: LookupMap<AccountId, Member>,
}

impl Default for Club {
    fn default() -> Self {
        Self { members: LookupMap::new(b"m") }
    }
}
Coming from EVM

Solidity packs fields into 32-byte slots because you pay gas per slot written. Borsh has no slots and no padding: each field costs exactly its size.

The difference is what that size costs: on Ethereum a one-off write fee, on NEAR NEAR locked for as long as the record exists, refundable when it is deleted.

Check yourself

3 questions · progress saved in this browser

  1. 1.How many bytes does one entry of a LookupMap<AccountId, u64> with a 1-byte prefix and the key bob.near count for storage?
  2. 2.Why is one LookupMap<AccountId, Profile> usually cheaper than three separate maps keyed by the same account?
  3. 3.What is the best way to store a token balance in contract state?