Gas Optimization in Solidity: Techniques That Actually Reduce Costs
Nitish Beejawat
Founder, Tantrija Enterprises
Contents
- 1Storage is the most expensive operation — design accordingly
- 2Calldata vs memory: know the difference
- 3Loops: the hidden gas sink
- 4Structural optimizations that matter more than micro-optimizations
- 5How to measure and verify your optimizations
Gas optimization is one of those areas where developers often spend time on micro-optimizations that save fractions of a percent while ignoring structural decisions that could cut costs by 50%. Here are the techniques that produce meaningful real-world savings.
Storage is the most expensive operation — design accordingly
Reading and writing to storage (SLOAD and SSTORE opcodes) are the most expensive EVM operations. SSTORE to a new slot costs 20,000 gas. Reading a storage slot costs 2,100 gas after EIP-2929.
The first optimization is storage packing. Multiple variables that together fit in 32 bytes can share a single storage slot. A uint128 and a bool can be packed together. The compiler packs variables declared consecutively, so order matters. Declaring uint128, bool, uint128 wastes a slot because the bool breaks the packing. Declaring uint128, uint128, bool packs the two uint128s together efficiently.
The second optimization is caching storage reads. If you read the same storage variable multiple times in a function, read it once into a local variable and use that. SLOAD at 2,100 gas per read versus MLOAD (memory read) at 3 gas is a 700x difference.
The third optimization: if you only need to write to a variable once and never read it in the same transaction, consider using events instead. Events store data in the transaction log rather than contract storage — significantly cheaper to write, and readable off-chain.
Calldata vs memory: know the difference
Function parameters can be declared as calldata or memory for reference types (arrays, structs). Calldata is read-only — you cannot modify it — but it is significantly cheaper than copying to memory.
For external functions where you do not need to modify the parameter: use calldata. For internal functions or when you need to modify the parameter: use memory. The gas savings from calldata can be significant for functions that receive large arrays.
The practical pattern: an external function that receives a large array for processing should declare it as calldata. If it calls an internal helper that needs to slice or modify it, copy only the relevant portions to memory there.
Memory itself has a non-linear expansion cost. Memory expansion in the EVM is cheap for the first few hundred bytes and becomes progressively more expensive. For functions that process large data structures in memory, this can be a meaningful cost. Prefer streaming processing over loading everything into memory when possible.
Loops: the hidden gas sink
Unbounded loops are a dangerous pattern in Solidity. A loop that iterates over an array without a length cap can run out of gas (transaction reverts), and the gas cost scales linearly with array size, making the function more expensive as state grows.
For loops over dynamic arrays: always have a maximum iteration cap. For operations that must process large arrays, use a pagination pattern where callers specify a start index and count.
The more subtle optimization: if your loop condition involves a storage read, cache it before the loop. A loop like "for (uint i = 0; i < array.length; i++)" reads array.length from storage on every iteration. Cache it as "uint len = array.length" before the loop and use len in the condition.
Avoid unnecessary state changes inside loops. If you can batch all the changes and apply them after the loop, do so. One SSTORE is always cheaper than N SSTOREs for the same slot.
Structural optimizations that matter more than micro-optimizations
The highest-impact gas optimizations are architectural, not syntactic.
Custom errors vs. require strings: require("Insufficient balance") stores the string on-chain and reverts with it. A custom error: "error InsufficientBalance(uint256 available, uint256 required)" costs significantly less because it is stored as a 4-byte selector. In high-frequency contract interactions, this adds up.
Immutable and constant variables: values known at deployment that never change should be declared as immutable (set in constructor, stored in bytecode) or constant (hardcoded). These are free to read — they do not consume storage slots.
The optimizer settings matter: the Solidity compiler optimizer with high runs (e.g., 200,000) optimizes for runtime execution cost at the expense of deployment cost. For contracts that will be called many times, high optimizer runs produce better runtime gas efficiency. For contracts called infrequently, lower runs or default settings are fine.
Bit manipulation: for uint256 variables used as bitmaps (storing many boolean flags), bit operations (AND, OR, XOR, shifts) on a single storage slot are far cheaper than storing each boolean in its own variable.
How to measure and verify your optimizations
Foundry's gas reporting is the best tool for measuring the actual gas impact of changes. Run "forge test --gas-report" to see gas costs per function call across your test suite. Make a change, run it again, and compare — do not trust intuition about what is cheaper.
Tenderly's transaction simulator lets you simulate transactions before sending them and get precise gas breakdowns by opcode. For optimizing a specific high-frequency function, analyzing the opcode-level gas consumption directly is the most effective approach.
The important caveat: premature gas optimization is real. Write correct, readable code first. Then profile with actual usage patterns to identify the functions that are called frequently and have high gas costs. Optimize those, not the functions that are called once a day.
For DeFi protocols where gas efficiency is a competitive differentiator, invest in a dedicated gas optimization review after the core logic is correct and audited. Changing gas optimization and security review simultaneously increases the risk of introducing vulnerabilities.
Nitish Beejawat
Founder, Tantrija Enterprises
Nitish Beejawat is the founder of Tantrija Enterprises and led core L1 protocol development on Layer One X — a custom Layer 1 blockchain built from scratch. He has 6+ years of production blockchain engineering experience across DeFi, enterprise blockchain, and custom chain development.
linkedin.com/in/nitish-beejawatNeed production-grade Solidity development?
Gas optimization, security, and correct economic design are all part of our standard process.
No sales pitch. Just an honest technical conversation.