Calldata vs. Memory vs. Storage in Solidity: What Each One Does and When to Use It

Calldata is the read-only area where an external function’s arguments sit while a Solidity transaction runs — the raw call data attached to the transaction itself, and the cheapest of Solidity’s three data locations. It’s the one you should reach for by default whenever a function only needs to read its inputs. Memory holds temporary values for the duration of a single call, and storage is the only one of the three that survives after the transaction ends. Every variable in a Solidity contract lives in one of these three places, and getting Solidity calldata wrong for a given function is one of the most common ways contracts burn gas for no reason.

This isn’t a cosmetic choice. The same function, written with memory instead of calldata for its parameters, can cost noticeably more gas per call — money your users pay, every single time. Here’s the breakdown: what calldata actually is, why Solidity forces this choice on you at all, who needs to care, and the gas math that makes one keyword worth more than it looks.

What Is Calldata in Solidity?

Calldata is the raw byte data attached to a transaction or an external message call — the encoded function selector plus arguments that get sent along when one contract calls another, or when a user calls a contract directly. Solidity exposes that raw data as a data location: a keyword you can assign to function parameters so the compiler knows to read straight from calldata instead of copying it anywhere else.

Data location: in Solidity, one of three places — storage, memory, or calldata — where a reference-type variable (arrays, structs, strings, mappings) physically lives during execution. See the official Solidity documentation on data locations for the compiler’s exact rules.

Here’s what that looks like in practice:

function isOwner(address[] calldata whitelist, address user) external view returns (bool) {
    for (uint i = 0; i < whitelist.length; i++) {
        if (whitelist[i] == user) return true;
    }
    return false;
}

That calldata keyword tells the compiler this function never needs to modify whitelist — it only reads it. Nothing gets copied into memory first; the function reads directly from the transaction data itself.

How Do Memory and Storage Differ From Calldata?

Storage is where a contract’s state variables actually live, written permanently to the blockchain and readable by anyone, forever, unless the contract itself overwrites it. Memory is a scratch space that exists only for the length of one external function call and gets wiped the moment that call finishes. Calldata sits apart from both: it isn’t written anywhere new at all, and it can’t be changed once the transaction starts.

The practical distinction comes down to three questions: does the data need to persist after this call ends, does the function need to modify it, and where did the data come from in the first place.

Data locationLifetimeMutable?Typical use
StoragePermanent, until overwrittenYesState variables, contract balances, mappings
MemoryOne function callYesLocal working variables, values built during execution
CalldataOne function call, read-onlyNoExternal function arguments that only get read

State variables default to storage automatically — you never write the keyword. Function parameters and local variables involving arrays, structs, or strings need an explicit location, and that’s the choice this whole guide is about.

Why Does Solidity Force You to Choose a Data Location?

Solidity requires this because the Ethereum Virtual Machine treats these three areas as physically separate memory systems, each with its own cost and behavior, and the compiler can’t guess which one you meant for a complex type. A uint256 fits in one stack slot, so the compiler handles it without asking. An array or a string doesn’t fit that way, so you tell the compiler exactly where it should read from or write to.

This is critical: every operation on the Ethereum network costs gas, and gas has to be predictable before a transaction runs, or the network can’t safely price it. Storage writes touch permanent blockchain state shared by every node, which is why they’re the most expensive operation by far. Memory and calldata cost less because nothing about them needs to persist past the current call.

Skip the data location on a parameter that needs one, and the compiler stops you before deployment with a TypeError: Data location must be 'memory' or 'calldata' for parameter in function, but none was given — not a runtime failure, but a hard compile-time rule, exactly as Solidity’s error handling documents it.

Who Actually Needs to Think About This?

Four groups run into this decision regularly, and for different reasons. Smart contract developers hit it on every function signature that takes an array, struct, or string — it’s not optional syntax, so understanding it isn’t optional either.

Gas optimization specialists and auditors look at data location choices specifically because they’re one of the highest-leverage, lowest-effort ways to cut a contract’s deployment and call costs. Teams building high-traffic contracts — DEX routers, NFT mints, anything called thousands of times a day — feel the cumulative gas difference directly in what their users pay. And anyone reviewing a smart contract audit will see data location flagged constantly, since it’s a pattern auditors check almost mechanically.

Storage vs. Memory vs. Calldata: What the Gas Actually Costs

This is where the choice stops being about syntax and starts mattering financially. Storage operations are priced through the EVM’s SSTORE and SLOAD opcodes, and the numbers aren’t close to calldata or memory.

Writing a storage variable from zero to a non-zero value costs 22,100 gas — 20,000 for the write itself plus a 2,100 cold-access charge under EIP-2929, the Berlin-hardfork rule that raised the cost of a contract’s first storage access in a transaction. Michael Amadi and Jesse Raymond, the RareSkills researchers behind The RareSkills Book of Solidity Gas Optimization, put the scale of that plainly: “Initializing a storage variable is one of the most expensive operations a contract can do,” they write in their gas optimization guide — and their most-repeated recommendation across 80-plus techniques is to avoid zero-to-non-zero storage writes wherever the logic allows it.

Calldata is priced completely differently, per byte rather than per storage slot. EIP-2028 dropped the cost of a non-zero calldata byte from 68 gas to 16 gas back in 2019, specifically to make data-heavy transactions and layer-2 rollups cheaper; a zero byte still costs 4 gas. Memory sits in between architecturally — reading or writing a memory word costs a small flat fee, plus a quadratic expansion cost that grows as a function uses more memory within a single call, which is why looping over a huge array copied into memory gets disproportionately expensive as the array grows.

LocationTypical gas costWhat drives the cost
Storage (SSTORE, zero → non-zero)22,100 gasPermanent state change, cold access
Storage (SLOAD)100–2,100 gasCold vs. warm access within a transaction
Calldata (per non-zero byte)16 gasFixed per-byte transaction data cost
Memory~3 gas + quadratic expansionGrows non-linearly with total memory used

The practical takeaway: if a function only reads its arguments, calldata almost always wins. It skips the copy into memory entirely, and the official Solidity docs are direct about this — the compiler documentation recommends using calldata “because it will avoid copies and also makes sure that the data cannot be modified” whenever a function doesn’t need to change the value.

When Should Function Parameters Use Calldata Instead of Memory?

The short version: use calldata for any external function parameter your code doesn’t modify, and reach for memory only when you actually need to change the value inside the function. Since Solidity 0.6.9, both keywords are legal on external, public, internal, and private functions alike, so there’s rarely a technical blocker — the choice mostly comes down to whether mutation is genuinely needed.

solidity

// Cheaper: read-only, external caller's data used as-is
function sumArray(uint256[] calldata values) external pure returns (uint256 total) {
    for (uint i = 0; i < values.length; i++) {
        total += values[i];
    }
}

// Necessary: the function needs to modify the array before returning it
function sortedCopy(uint256[] memory values) public pure returns (uint256[] memory) {
    // sorting logic that reorders `values` in place
    return values;
}

Internal functions called from within the same contract can also take storage references directly, avoiding a copy altogether when the goal is reading or updating state that’s already there. That’s a fourth pattern worth knowing, even though it’s not one of the three data locations for arguments in the strictest sense — it’s storage, just accessed by reference instead of by value.

Why Doesn’t Solidity Constructor Calldata Exist?

Here’s the part that catches people off guard: Solidity constructor calldata parameters aren’t allowed at all. Try constructor(string calldata _name) and the compiler rejects it — constructors only accept memory for arrays, structs, and strings, never calldata.

The reason is architectural, not arbitrary. A normal function call has its arguments encoded directly in the transaction’s calldata, which the EVM reads with the CALLDATALOAD opcode. Contract creation works differently: constructor arguments get ABI-encoded and appended to the very end of the deployment bytecode itself, after the contract’s runtime code. The EVM’s init code then calculates the argument length using CODESIZE and pulls the data out with CODECOPY, not CALLDATACOPY — because during deployment, there’s no separate calldata channel the way there is for a message call to an already-deployed contract.

That’s a real trade-off, not just trivia. Calldata results in cheaper transactions for the user, but that saving genuinely isn’t available at deployment time — constructor arguments always pay the memory-copy cost, no matter how the code is written.

Common Mistakes That Waste Gas on Data Locations

A handful of patterns account for most of the avoidable gas waste here. Copying a calldata array into memory just to pass it to another internal function that could accept calldata directly is one of the most frequent — it pays for a copy the code never needed.

Using memory on an external function’s array or string parameter out of habit, when the body never modifies it, is another. So is reading the same storage variable repeatedly inside a loop instead of caching it once locally — each SLOAD gets billed separately, even against the same slot. None of these mistakes break a contract; they just mean users pay more than the logic requires, which is exactly the kind of gap an independent security and gas audit is built to catch before mainnet.

How Do You Check Gas Costs Before You Deploy?

The only reliable way to know whether a data location choice actually saved gas is to test it against real network state before shipping. Simulating a transaction with eth_call or a full debug_traceCall shows the exact gas a function consumes, without spending anything or waiting for a block.

That check depends on RPC access to current chain state. NOWNodes, for instance, offers Ethereum RPC and Debug/Trace API access across mainnet and testnets, so a developer can compare calldata against memory, or one storage layout against another, against live state without running a self-hosted debug-enabled node.

The same read-only principle shows up one layer up the stack, too. Meta-transaction relayers append the real sender’s address to the end of a forwarded call’s calldata specifically because calldata is cheap to extend and easy for a receiving contract to parse — the same cost profile that makes it the right default for ordinary read-only function arguments.

Conclusion

Calldata, memory, and storage aren’t interchangeable syntax — they’re three different places with three different lifetimes and three very different price tags. Storage is for what genuinely needs to survive on-chain. Memory is for values a function builds and discards within one call. Calldata is for external arguments a function only needs to read, and it should be the default whenever nothing forces a copy.

Get comfortable with which is which, and one keyword choice on a function signature stops being a syntax detail and starts being a real, measurable saving passed on to whoever calls that function. The constructor exception is worth remembering precisely because it’s the one place that saving isn’t available — a small architectural quirk that makes more sense once you know what CODECOPY is doing behind it.

FAQ

Can You Change a Calldata Variable Inside a Function?

No. Calldata is explicitly non-modifiable — any attempt to write to a calldata array or struct fails at compile time. If a function needs to alter the data, it has to copy the relevant part into memory first.

Is Calldata Only Available for External Functions?

No, not since Solidity 0.6.9. Before that release, calldata was restricted to external function parameters; today, both memory and calldata are legal data locations on external, public, internal, and private functions alike.

Does Using Calldata Instead of Memory Ever Make a Function Slower?

No — calldata access is at least as fast as memory access at the EVM level, and it skips a copy operation memory requires. The gas savings and any performance difference both point the same direction, toward calldata for read-only data.

What Happens If You Try to Return a Calldata Variable From a Function?

It works, since returning a value doesn’t modify it — a function can accept a calldata array and return it, or a slice of it, without ever copying it into memory first.

Do Public Functions Default to Memory or Calldata?

Neither is chosen automatically. Every reference-type parameter on a public or external function needs an explicit memory or calldata keyword; leaving it out is a compile error, not a default.