Understanding EVM Gas: A Developer's Complete Guide

Master EVM gas costs, gas limits, EIP-1559 fee mechanics, per-opcode pricing, and optimization techniques. Practical guide for smart contract developers.

By Zig EVM Team |

TL;DR: Gas is Ethereum’s metering system that prices every computational step in the EVM. Each opcode has a fixed gas cost — from 3 gas for ADD to 20,000 gas for a cold SSTORE. EIP-1559 introduced a base fee that adjusts with demand plus an optional priority tip. Understanding gas costs at the opcode level is the foundation of smart contract optimization. This guide covers how gas works, what each operation costs, how the fee market functions, and practical techniques to reduce gas consumption in your contracts.

What Gas Is and Why It Exists

Gas is the fundamental resource metering mechanism of the Ethereum Virtual Machine. Every operation the EVM executes — every addition, every memory read, every storage write — consumes a measured amount of gas. The sender of a transaction pays for this gas in ETH.

Gas exists to solve three critical problems:

  1. Halting problem: The EVM is Turing-complete (in principle). Without gas, a contract containing an infinite loop would run forever, halting the network. Gas guarantees that every execution terminates: when gas runs out, execution stops.

  2. Resource pricing: Validators (formerly miners) expend real computational resources — CPU cycles, memory, disk I/O, bandwidth — to process transactions. Gas converts these abstract costs into a concrete price that compensates validators fairly.

  3. Spam prevention: Without a cost for computation, an attacker could flood the network with expensive operations at zero cost. Gas makes attacks economically prohibitive.

Gas Costs by Opcode Category

The EVM assigns gas costs to opcodes based on the computational resources they consume. The Yellow Paper and subsequent EIPs define these costs precisely.

Tier 0: Zero Cost

OpcodeGasNotes
STOP0Halts execution
RETURN0Returns data (memory cost separate)
REVERT0Reverts with data (memory cost separate)

These opcodes are free because they terminate execution — no further computation occurs.

Tier 1: Base Operations (2-3 Gas)

OpcodeGasCategory
ADD, SUB3Arithmetic
LT, GT, EQ, ISZERO3Comparison
AND, OR, XOR, NOT3Bitwise
PUSH1-323Stack
DUP1-163Stack
SWAP1-163Stack
POP2Stack
CALLER, CALLVALUE2Environment
TIMESTAMP, NUMBER2Block info
PC, MSIZE2Program state

These are the cheapest operations because they involve only stack or register manipulation with no external data access.

Tier 2: Moderate Operations (5-10 Gas)

OpcodeGasCategory
MUL, DIV, MOD5Arithmetic
SDIV, SMOD5Signed arithmetic
SIGNEXTEND5Arithmetic
ADDMOD, MULMOD8Modular arithmetic
JUMP8Flow control
JUMPI10Flow control

Multiplication and division cost more than addition because they are computationally more expensive, even on modern hardware.

Tier 3: Memory Operations (3 + Expansion Cost)

Memory opcodes have a base cost of 3 gas plus an expansion cost that grows quadratically:

memory_cost(word_count) = 3 * word_count + floor(word_count^2 / 512)

The expansion cost is calculated as the difference between the new memory cost and the previous memory cost. Here is what that looks like in practice:

Memory UsedWord CountTotal Memory CostMarginal Cost
32 bytes133
256 bytes82421
1 KB329874
4 KB128416318
32 KB1,0245,1204,704
1 MB32,7682,195,456

The quadratic term makes large memory allocations prohibitively expensive. This is by design: it prevents contracts from consuming unbounded amounts of memory on validator nodes.

// Zig EVM memory cost calculation
pub fn calculateMemoryCost(size_bytes: u64) u64 {
    if (size_bytes == 0) return 0;
    const words = (size_bytes + 31) / 32;
    return 3 * words + (words * words) / 512;
}

// Expansion cost = new_cost - old_cost
pub fn expansionCost(old_size: u64, new_size: u64) u64 {
    if (new_size <= old_size) return 0;
    return calculateMemoryCost(new_size) - calculateMemoryCost(old_size);
}

Tier 4: Storage Operations (100-20,000 Gas)

Storage is the most expensive category because it modifies persistent blockchain state.

OperationGas CostCondition
SLOAD (warm)100Slot already accessed this tx
SLOAD (cold)2,100First access to slot in tx
SSTORE: zero to non-zero20,000Creating new storage entry
SSTORE: non-zero to non-zero5,000Modifying existing entry
SSTORE: non-zero to zero5,000Clearing entry (+ 4,800 refund)

A single SSTORE writing a new value costs 20,000 gas — equivalent to more than 6,600 ADD operations. This ratio reflects the true cost difference: an ADD touches only the stack in memory, while an SSTORE permanently changes the global state trie that every node must store.

Tier 5: External Operations (Variable)

OperationBase GasNotes
BALANCE (cold)2,600Reading another account’s balance
BALANCE (warm)100Already accessed this tx
EXTCODESIZE (cold)2,600Reading another contract’s code size
CALL100 + value + memoryPlus 2,600 if cold address
CREATE32,000Plus code deposit cost
CREATE232,000Plus hashing cost
SELFDESTRUCT5,000+Plus 25,000 if sending to new account
LOG0-4375 + 8*bytes + topicsEvent emission
SHA3/KECCAK25630 + 6*wordsHashing

Gas Limit vs. Gas Price

These two concepts are frequently confused:

Gas Limit is the maximum number of gas units a transaction may consume. If execution requires more gas than the limit, the transaction reverts and all state changes are rolled back — but the sender still pays for the gas consumed. Setting the gas limit too low causes failures; setting it too high merely reserves more ETH upfront (the unused portion is refunded).

Gas Price (pre-EIP-1559) or Max Fee Per Gas (post-EIP-1559) is the price in wei that the sender pays per unit of gas.

Total cost in ETH = gas_used * effective_gas_price / 10^18

Example: a simple token transfer using 65,000 gas at 20 gwei (20 * 10^9 wei):

65,000 * 20,000,000,000 = 1,300,000,000,000,000 wei = 0.0013 ETH

EIP-1559: The Modern Fee Market

Before EIP-1559 (August 2021), Ethereum used a first-price auction: users bid gas prices, and validators selected the highest-paying transactions. This was unpredictable — gas prices could spike 10x within minutes.

EIP-1559 replaced this with a more predictable system:

Base Fee

The base fee is set algorithmically by the protocol. It adjusts up or down by a maximum of 12.5% per block based on how full the previous block was relative to a target (50% of the gas limit).

  • Block more than 50% full: base fee increases.
  • Block less than 50% full: base fee decreases.
  • Block exactly 50% full: base fee unchanged.

The base fee is burned (destroyed), not paid to validators. This makes ETH potentially deflationary.

Priority Fee (Tip)

The priority fee (or tip) is an optional additional payment that goes directly to the validator. It incentivizes validators to include your transaction. During periods of low demand, a priority fee of 1-2 gwei is typical. During congestion, higher tips are needed.

Max Fee Per Gas

Users specify a max fee per gas — the absolute maximum they are willing to pay. The actual cost is:

effective_gas_price = min(max_fee_per_gas, base_fee + max_priority_fee)
refund = (max_fee_per_gas - effective_gas_price) * gas_used
// In a transaction:
// maxFeePerGas: 30 gwei
// maxPriorityFeePerGas: 2 gwei
// Current base fee: 15 gwei
//
// Effective price: min(30, 15 + 2) = 17 gwei
// Validator receives: 2 gwei * gas_used
// Burned: 15 gwei * gas_used
// Refunded: (30 - 17) * gas_used = 13 gwei * gas_used

How Zig EVM Meters Gas

The Zig EVM project implements gas metering that closely follows the Ethereum specification. Gas is checked and deducted before each opcode executes:

// Gas metering in the Zig EVM execution loop
pub fn execute(self: *EVM) !void {
    while (self.pc < self.bytecode.len) {
        const opcode = self.bytecode[self.pc];

        // Look up gas cost for this opcode
        const gas_cost = getGasCost(opcode);

        // Check if enough gas remains
        if (self.gas_remaining < gas_cost) {
            return error.OutOfGas;
        }

        // Deduct gas before execution
        self.gas_remaining -= gas_cost;

        // Dispatch to opcode implementation
        const impl = self.opcodes.get(opcode) orelse return error.InvalidOpcode;
        try impl.execute(self);

        self.pc += 1;
    }
}

This pre-deduction model is important: if an opcode fails, the gas it would have consumed is already deducted. This matches Ethereum’s behavior where a reverted transaction still costs gas.

You can observe gas consumption in real time using the Zig EVM Playground, which displays the gas cost of each instruction as it executes.

Gas Optimization Techniques

1. Minimize Storage Writes

Storage writes dominate gas costs in most contracts. Every optimization effort should start here.

// BAD: Multiple storage writes
function updateThreeValues(uint a, uint b, uint c) external {
    value1 = a;  // 5,000-20,000 gas
    value2 = b;  // 5,000-20,000 gas
    value3 = c;  // 5,000-20,000 gas
}

// BETTER: Pack into a single slot if values fit
// Three uint80 values fit into one 256-bit slot
function updatePacked(uint80 a, uint80 b, uint80 c) external {
    packedValues = uint256(a) | (uint256(b) << 80) | (uint256(c) << 160);
    // Single SSTORE: 5,000-20,000 gas total
}

2. Use Storage Packing

Solidity stores variables in 32-byte slots. Variables smaller than 32 bytes that are declared consecutively are packed into the same slot.

// BAD: 3 storage slots (3 * SLOAD to read all)
contract Unpacked {
    uint256 a;  // slot 0
    uint8 b;    // slot 1
    uint256 c;  // slot 2
}

// GOOD: 2 storage slots
contract Packed {
    uint256 a;  // slot 0
    uint256 c;  // slot 1
    uint8 b;    // slot 1 (packed with c if c were smaller,
                //         but here it gets its own slot)
}

// BEST: 2 storage slots with actual packing
contract WellPacked {
    uint256 a;  // slot 0
    uint8 b;    // slot 1
    uint8 c;    // slot 1 (packed with b)
    // 30 bytes remaining in slot 1
}

3. Use calldata Instead of memory for Read-Only Inputs

// More expensive: copies data to memory
function process(uint[] memory data) external { ... }

// Cheaper: reads directly from transaction calldata
function process(uint[] calldata data) external view { ... }

4. Short-Circuit Expensive Operations

// Check the cheap condition first
function validate(address user, bytes32 proof) external view {
    require(user != address(0), "zero address");     // 3 gas (ISZERO)
    require(balances[user] > 0, "no balance");       // 2,100 gas (SLOAD)
    require(verifyProof(proof), "invalid proof");     // 30+ gas (SHA3)
}

5. Use Events Instead of Storage for Historical Data

// Expensive: storing every transfer in an array
Transfer[] public transfers;
function recordTransfer(address to, uint amount) internal {
    transfers.push(Transfer(to, amount)); // SSTORE: 20,000 gas
}

// Cheap: emitting an event (indexed off-chain)
event Transfer(address indexed to, uint amount);
function recordTransfer(address to, uint amount) internal {
    emit Transfer(to, amount); // LOG2: ~1,125 gas
}

6. Batch Operations

// Expensive: N separate transactions, each with 21,000 base cost
for (uint i = 0; i < recipients.length; i++) {
    // External call per recipient
}

// Cheaper: single transaction with a loop
function batchTransfer(address[] calldata recipients, uint[] calldata amounts) external {
    for (uint i = 0; i < recipients.length; i++) {
        _transfer(recipients[i], amounts[i]);
    }
}
// Saves (N-1) * 21,000 gas in base transaction costs

Gas Refunds

The EVM provides gas refunds in specific situations:

  • Clearing storage: Setting a storage value from non-zero to zero earns a 4,800 gas refund.
  • Self-destruct: Destroying a contract earns a 24,000 gas refund (deprecated after EIP-6780).

Refunds are capped at 20% of the total gas consumed by the transaction (reduced from 50% in EIP-3529). This cap was introduced to prevent refund-based gas token exploits.

max_refund = gas_used / 5
actual_refund = min(accumulated_refund, max_refund)
effective_gas_used = gas_used - actual_refund

Intrinsic Gas: The Floor Cost

Every transaction has a minimum gas cost before any bytecode executes:

ComponentGas Cost
Base transaction21,000
Contract creation53,000 (32,000 + 21,000)
Zero byte of calldata4
Non-zero byte of calldata16
Access list entry (address)2,400
Access list entry (storage key)1,900

A simple ETH transfer with no data costs exactly 21,000 gas. A contract call with 100 bytes of non-zero calldata costs at least 21,000 + 100 * 16 = 22,600 gas before the first opcode executes.

Estimating Gas for a Bytecode Sequence

Let us calculate the gas cost for a bytecode sequence that adds two numbers and stores the result in memory:

# Bytecode: 600560070160005260206000F3
# Instruction     Gas    Running Total
# PUSH1 0x05       3          3
# PUSH1 0x07       3          6
# ADD              3          9
# PUSH1 0x00       3         12
# MSTORE           3+3       18  (3 base + 3 for memory expansion to 32 bytes)
# PUSH1 0x20       3         21
# PUSH1 0x00       3         24
# RETURN           0         24
#
# Total execution gas: 24
# Plus intrinsic cost: 21,000 + (13 bytes * 16 per non-zero byte) = 21,208
# Grand total: ~21,232 gas

Try this in the Zig EVM Playground to see the per-instruction gas consumption as the program executes.

Key Takeaways

  • Gas is Ethereum’s metering system, ensuring termination, fair resource pricing, and spam prevention.
  • Opcode costs range from 0 gas (STOP, RETURN) to 20,000 gas (SSTORE for new values), reflecting actual computational expense.
  • Memory costs grow quadratically, making large allocations prohibitively expensive by design.
  • Storage operations dominate gas costs in most contracts: minimizing SSTORE calls is the single most impactful optimization.
  • EIP-1559 replaced unpredictable gas auctions with an algorithmic base fee (burned) plus a priority tip (paid to validators).
  • Gas refunds for clearing storage are capped at 20% of total gas consumed.
  • Every transaction pays at least 21,000 gas before any bytecode executes, plus 4-16 gas per calldata byte.
  • Practical optimization strategies include storage packing, using calldata over memory, short-circuiting checks, and batching operations.
  • The Zig EVM implements accurate gas metering following the Ethereum specification, and you can observe it in action in the Playground.

Further Reading

Frequently Asked Questions

What is gas in Ethereum?
Gas is the unit that measures the computational effort required to execute operations on Ethereum. Every opcode, storage access, and transaction has a gas cost. Users pay gas fees in ETH to compensate validators for the resources consumed. Gas ensures that the network cannot be overwhelmed by expensive computations.
Why does SSTORE cost so much gas?
SSTORE (storage write) costs 5,000 to 20,000 gas because it permanently modifies blockchain state. Every full node must store this data indefinitely, and the storage trie must be updated and re-hashed. The high cost reflects the long-term burden on the network, not just the immediate computation.
What is the difference between gas limit and gas price?
The gas limit is the maximum amount of gas a transaction is allowed to consume. The gas price (or max fee per gas in EIP-1559) is the amount of ETH the sender is willing to pay per unit of gas. Total cost = gas_used * effective_gas_price. If execution exceeds the gas limit, the transaction reverts.
How does EIP-1559 change gas pricing?
EIP-1559 replaced the simple auction model with a base fee that adjusts algorithmically based on network demand, plus an optional priority fee (tip) to incentivize validators. The base fee is burned, reducing ETH supply. Users set a max fee per gas, and any difference between the max fee and actual cost is refunded.
How can I reduce gas costs in my smart contracts?
Key strategies include: minimize storage writes (SSTORE), use mappings instead of arrays for lookups, pack multiple values into single storage slots, use calldata instead of memory for read-only function inputs, short-circuit conditionals to avoid unnecessary computation, and batch operations to amortize fixed costs.

Try Zig EVM

Explore the interactive playground or dive into the source code.