How Solidity Mappings Store Data Under the Hood
When developers first use mapping in Solidity, it feels like magic. You write one line and suddenly have a fast, gas-efficient key-value store:
mapping(address => uint256) balances;But how does this actually work inside Ethereum? Where do the keys and values live, and why can’t you loop over a mapping? To answer these questions, we need to look at how the EVM handles storage at its lowest level.
How Storage Works in the EVM
The Ethereum Virtual Machine doesn’t understand mappings, arrays, or structs. At its core, it only knows one thing: a giant flat key-value database of the form uint256 → uint256.
Every contract has access to ²²⁵⁶ possible storage slots. Each slot has a 256-bit index and stores exactly 32 bytes of data. Conceptually, storage looks like this:
slot 0 → value
slot 1 → value
slot 2 → value
...
slot 2^256-1 → valueThere are no “lists” or “tables” — just numbered slots. If a slot was never written to, it returns 0 by default. Everything Solidity does — mappings, arrays, structs — is built on top of this deceptively simple model.
How Normal Variables Are Stored
Consider this contract:
contract Example {
uint256 x;
uint256 y;
}Solidity assigns storage slots in declaration order: x gets slot 0, y gets slot 1. If x = 5, then slot 0 simply contains 5. Straightforward enough. Now let's see what happens when a mapping enters the picture.
How Solidity Assigns a Mapping Slot
contract Example {
mapping(address => uint256) balances;
}Solidity still assigns a slot — balances gets slot 0. But here's the important part: that slot does not store any mapping data. It's only a base slot — a starting point used to compute where individual values will end up. Think of it as a namespace identifier for the mapping.
How Mapping Values Are Stored and Retrieved
When you write balances[alice] = 100;, Solidity doesn't store Alice's address, a key-value pair, or any kind of record. Instead, it performs a series of deterministic steps:
- Pad the key — Alice’s address (20 bytes) is left-padded to 32 bytes.
- Pad the base slot — The base slot number (e.g.,
0) is also padded to 32 bytes. - Hash them together — Solidity computes
keccak256(padded_key ++ padded_base_slot), producing a 32-byte hash like0x8F2A...91BC. - Store the value at that hash — The EVM writes
storage[0x8F2A...91BC] = 100.
That’s it. Only the value is stored — the key is never saved anywhere.
When you later read balances[alice], Solidity simply repeats the same process: pad Alice, pad the base slot, compute the hash, and read from that slot. Because identical inputs always produce the same hash, you always land on the correct slot. No searching, no scanning, no iteration — just math.
Why Keys Are Not Stored
This is the part that trips up a lot of developers. Mappings don’t store keys because they don’t need to. The key is consumed by the hash function to produce a storage location, and that location itself is the representation of the key.
Instead of storing traditional (key, value) pairs, Solidity stores:
hash(key, base_slot) → valueThis is exactly why you cannot list all keys, iterate over a mapping, or ask “how many items are inside.” No list of keys exists on-chain. The mapping is a one-way function, not a data structure you can inspect.
Why Hashing Is Necessary
You might wonder: “Why not just use the key directly as the slot index?”
Get Aryanpandey’s stories in your inbox
Join Medium for free to get updates from this writer.
Because that would create serious problems:
- Cross-mapping collisions — Without hashing, a key like
0x0000...0001used across two different mappings in the same contract could resolve to the same raw storage slot, causing them to silently overwrite each other. By including the base slot in the hash input, Solidity gives every mapping its own isolated namespace. - Inconsistent key sizes — Keys can be
address,uint256,bytes32, and other fixed-size types. Hashing normalizes all of them into a uniform 32-byte slot index. - Uniform distribution —
keccak256spreads values across the entire 2²⁵⁶ address space, effectively eliminating clustering or accidental conflicts between unrelated storage variables.
Why Mapping Access Is O(1)
When you access a mapping value, Solidity does exactly one thing: compute a hash and read one slot. This operation never depends on how many entries exist in the mapping, how many users your contract has, or how large the overall state is.
The number of steps is always constant. That’s what makes mapping access O(1) — it’s direct indexing through a hash function, not a search through a data structure.
Why Unset Values Return Zero
If you read balances[bob] and Bob was never stored, Solidity still computes a slot for him. But since nothing was ever written there, the EVM returns the default value: 0. This is a property of EVM storage itself — every slot starts at zero. It's why mappings appear "pre-filled" with zeros for every possible key.
Nested Mappings
For a nested mapping like:
mapping(address => mapping(address => uint256)) allowance;Solidity simply applies the same hashing process multiple times:
slot1 = keccak256(owner ++ base_slot)
slot2 = keccak256(spender ++ slot1)The value is stored at slot2. Same idea, just layered — each level of nesting adds one more round of hashing.
Benefits and Tradeoffs
This design gives Solidity mappings some powerful properties:
- Constant-time access — Every lookup is fast and predictable, which is critical for gas costs. This is why ERC20 balances, NFT ownership, and DeFi allowances all rely on mappings.
- No wasted storage — Only entries that are explicitly written consume storage. Unused keys cost nothing.
- Collision resistance —
keccak256makes accidental collisions practically impossible across the 2²⁵⁶ slot space. - Massive scale — The design handles millions of entries without any degradation in performance.
But these benefits come with deliberate tradeoffs:
- No enumeration — You can’t write
for all users in balancesbecause there's no on-chain list. If you need to iterate over keys, you have to maintain a separate array, which costs extra gas. - No introspection — You can’t check a mapping’s size or ask whether a key exists (other than checking if the value is non-zero).
- Off-chain reconstruction — Most production dApps reconstruct full mapping state using event logs, indexers, or subgraphs rather than attempting on-chain iteration.
The Mental Model
At its core, a Solidity mapping is not a table or a dictionary. It’s a deterministic hash function over contract storage. The entire mechanism boils down to one formula:
value = storage[ keccak256(key ++ base_slot) ]That formula is the mapping. Everything else — the syntax, the bracket notation, the type declarations — is just Solidity making this formula ergonomic to use. Once you internalize this, a lot of Solidity’s quirks suddenly make sense.





