
Solidity Gas Optimization: 15 Patterns to Reduce Costs (2026)
Solidity gas optimization reduces the cost of deploying and running smart contracts on Ethereum. The most impactful patterns include caching storage reads, using custom errors instead of require strings, declaring variables as immutable, packing storage variables, and using unchecked arithmetic blocks. This guide covers 15 proven patterns with before-and-after code examples. Use the free Blockhertz AI Gas Optimizer to apply all optimizations automatically in under 60 seconds.
Why Gas Optimization Matters in 2026
Every operation in a Solidity smart contract costs gas. Every gas unit costs real money. On Ethereum mainnet, a single complex transaction can cost $5 to $50 depending on network conditions. For protocols processing thousands of transactions daily, unoptimized contracts waste hundreds of thousands of dollars per year.
Gas optimization is not premature optimization — it is a professional responsibility. Users pay for every inefficiency you leave in your code.
In 2026, three factors make gas optimization more important than ever:
- Higher user expectations: Users compare gas costs across competing protocols
- More competition: Gas-efficient protocols win users from inefficient ones
- Audit requirements: Security auditors now flag gas inefficiencies alongside vulnerabilities
Understanding Gas Costs
Before optimizing, understand what operations cost the most gas:
| Operation | Gas Cost | Notes |
|---|---|---|
| SSTORE (new value) | 20,000 | Writing new storage slot |
| SSTORE (update) | 2,900 | Updating existing slot |
| SLOAD | 800 | Reading from storage |
| MLOAD/MSTORE | 3 | Reading/writing memory |
| ADD/SUB/MUL | 3-5 | Basic arithmetic |
| External call | 2,600+ | Calling another contract |
| Base transaction | 21,000 | Every transaction minimum |
Key insight: Storage operations (SLOAD/SSTORE) are 100-300x more expensive than memory operations. Every optimization strategy targets reducing storage reads and writes.
The 15 Gas Optimization Patterns
Pattern 1: Cache Storage Reads in Memory
Gas saved: ~800 gas per eliminated SLOAD
The most impactful optimization in most contracts. Every time you read from storage costs 800 gas. Reading the same storage variable multiple times in one function is the most common gas waste.
// BEFORE — 3 SLOADs = 2,400 gas
function processBalance() external {
require(balances[msg.sender] > 0, "No balance");
emit BalanceRead(balances[msg.sender]);
totalProcessed += balances[msg.sender];
}
// AFTER — 1 SLOAD = 800 gas
function processBalance() external {
uint256 balance = balances[msg.sender]; // cache once
require(balance > 0, "No balance");
emit BalanceRead(balance);
totalProcessed += balance;
}
Pattern 2: Use Custom Errors Instead of Require Strings
Gas saved: ~50 gas per revert + smaller bytecode
String error messages in require statements are stored in bytecode and cost gas both at deployment and when the revert is triggered. Custom errors eliminate this cost.
// BEFORE — string stored in bytecode
require(amount > 0, "Amount must be positive");
require(msg.sender == owner, "Not authorized");
// AFTER — no string storage
error AmountMustBePositive();
error NotAuthorized();
if (amount == 0) revert AmountMustBePositive();
if (msg.sender != owner) revert NotAuthorized();
Custom errors also make your error messages ABI-decodable, improving debugging in production.
Pattern 3: Declare Variables as Immutable
Gas saved: ~797 gas per read (immutable = 3 gas vs storage = 800 gas)
Variables set once in the constructor and never changed should be declared immutable. They are stored in contract bytecode rather than storage, making reads nearly free.
// BEFORE — storage read = 800 gas
address public owner;
uint256 public maxSupply;
constructor(address _owner, uint256 _maxSupply) {
owner = _owner;
maxSupply = _maxSupply;
}
// AFTER — bytecode read = 3 gas
address public immutable owner;
uint256 public immutable maxSupply;
constructor(address _owner, uint256 _maxSupply) {
owner = _owner;
maxSupply = _maxSupply;
}
Pattern 4: Pack Storage Variables
Gas saved: Up to 15,000 gas on deployment, 2,000+ gas per write
Ethereum storage is organized in 32-byte slots. Multiple small variables can share one slot if declared together, reducing the number of storage operations needed.
// BEFORE — 3 storage slots = 3 SSTOREs
uint256 userId; // slot 0 (32 bytes)
uint256 balance; // slot 1 (32 bytes)
uint256 timestamp; // slot 2 (32 bytes)
// AFTER — 1 storage slot = 1 SSTORE
uint128 userId; // slot 0 (first 16 bytes)
uint64 balance; // slot 0 (next 8 bytes)
uint64 timestamp; // slot 0 (last 8 bytes)
Rule: Group variables of smaller types together. Place them consecutively so the compiler can pack them into shared slots.
Pattern 5: Use ++i Instead of i++ in Loops
Gas saved: ~5 gas per iteration
i++ creates a temporary variable holding the old value before incrementing. ++i increments first without the temporary variable. Small per iteration but meaningful in large loops.
// BEFORE
for (uint256 i = 0; i < array.length; i++) {
// loop body
}
// AFTER
for (uint256 i; i < array.length; ++i) {
// loop body
}
// Also: initialize as 0 by default
// uint256 i; is same as uint256 i = 0;
// but saves a tiny amount of gas
Pattern 6: Cache Array Length in Loops
Gas saved: ~100 gas per loop iteration for storage arrays
Accessing array.length in a loop condition reads from storage on every iteration if the array is a storage variable. Cache the length before the loop.
// BEFORE — reads array.length every iteration
for (uint256 i; i < storageArray.length; ++i) {
process(storageArray[i]);
}
// AFTER — reads length once
uint256 len = storageArray.length;
for (uint256 i; i < len; ++i) {
process(storageArray[i]);
}
Pattern 7: Use Unchecked Arithmetic Blocks
Gas saved: ~20-30 gas per operation
Solidity 0.8.0+ adds automatic overflow/underflow checks to every arithmetic operation. When you can mathematically prove overflow is impossible, use unchecked to skip these checks.
// BEFORE — checked arithmetic (safe but expensive)
for (uint256 i; i < len; ++i) {
total += values[i];
}
// AFTER — unchecked (safe because i < len always)
for (uint256 i; i < len; ) {
total += values[i];
unchecked { ++i; }
}
// Safe because: i starts at 0, increments by 1,
// and loop exits before i reaches type max
Warning: Only use unchecked when you have manually verified overflow cannot occur. Never use it blindly.
Pattern 8: Use calldata Instead of memory for Read-Only Parameters
Gas saved: ~200-500 gas per function call depending on data size
Function parameters marked memory are copied into memory. Parameters marked calldata are read directly from the call data without copying. Use calldata for any parameter you only read, never modify.
// BEFORE — copies data into memory
function processData(
bytes memory data,
uint256[] memory values
) external {
// read-only operations
}
// AFTER — reads directly from calldata
function processData(
bytes calldata data,
uint256[] calldata values
) external {
// same read-only operations, cheaper
}
Pattern 9: Use external Instead of public
Gas saved: ~10-50 gas per call
public functions can be called both internally and externally. external functions can only be called from outside the contract. External functions are cheaper because they read parameters directly from calldata.
// BEFORE — public (can be called internally)
function transfer(address to, uint256 amount)
public {
// function body
}
// AFTER — external (only external calls)
function transfer(address to, uint256 amount)
external {
// same function body, cheaper for external callers
}
Pattern 10: Short-Circuit Boolean Evaluation
Gas saved: Varies — skip expensive checks when cheap checks fail
Solidity evaluates && and || left to right and stops early when the result is determined. Put cheaper checks first so expensive operations are skipped when possible.
// BEFORE — always runs both checks
require(
isWhitelisted(msg.sender) &&
balances[msg.sender] >= amount
);
// AFTER — balance check (cheap) runs first
// isWhitelisted (expensive external call)
// only runs if balance check passes
require(
balances[msg.sender] >= amount &&
isWhitelisted(msg.sender)
);
Pattern 11: Avoid Storing Redundant Data
Gas saved: 20,000 gas per eliminated storage slot
Every new storage slot costs 20,000 gas to write. Never store data that can be computed from other stored data.
// BEFORE — stores total (redundant)
uint256 public total;
mapping(address => uint256) public balances;
function deposit(uint256 amount) external {
balances[msg.sender] += amount;
total += amount; // ← unnecessary SSTORE
}
// AFTER — compute total when needed
mapping(address => uint256) public balances;
// Compute total off-chain by reading events
// Or use a subgraph to track it
Pattern 12: Use Events Instead of Storage for Historical Data
Gas saved: ~15,000-18,000 gas per record
Storing historical data on-chain is extremely expensive. Emit events instead — they are stored in transaction logs at a fraction of the cost and are equally accessible to frontends.
// BEFORE — stores full history on-chain
struct Transfer {
address from;
address to;
uint256 amount;
uint256 timestamp;
}
Transfer[] public transferHistory; // ← very expensive
// AFTER — emit events (same data, 98% cheaper)
event Transfer(
address indexed from,
address indexed to,
uint256 amount,
uint256 timestamp
);
// Frontend reads events via getLogs()
// The Graph indexes them automatically
Pattern 13: Avoid Zero to Non-Zero Storage Writes
Gas saved: Up to 15,000 gas per write
Writing a non-zero value to a zero storage slot costs 20,000 gas. Writing to an already non-zero slot costs only 2,900 gas. Initialize storage to non-zero values when possible.
// BEFORE — starts at 0, first write costs 20,000
uint256 public counter; // = 0 by default
// AFTER — starts at 1, all writes cost 2,900
uint256 public counter = 1; // non-zero initial value
// Pattern: use (value - 1) in logic to preserve
// zero-means-unset semantics while avoiding
// zero-to-nonzero writes
Pattern 14: Batch Operations
Gas saved: 21,000 gas base fee per eliminated transaction
Every transaction on Ethereum costs a minimum of 21,000 gas base fee. Combining multiple operations into one transaction eliminates this overhead cost.
// BEFORE — 3 transactions = 63,000 gas base fee
await contract.approve(spender, amount1);
await contract.approve(spender, amount2);
await contract.approve(spender, amount3);
// AFTER — 1 transaction = 21,000 gas base fee
function batchApprove(
address[] calldata spenders,
uint256[] calldata amounts
) external {
uint256 len = spenders.length;
for (uint256 i; i < len; ++i) {
_approve(msg.sender, spenders[i], amounts[i]);
}
}
Pattern 15: Use Mappings Instead of Arrays for Lookups
Gas saved: O(1) vs O(n) lookup cost
Arrays require iterating to find elements, costing gas proportional to array size. Mappings provide O(1) constant-time lookups regardless of data size.
// BEFORE — O(n) lookup, expensive at scale
address[] public whitelist;
function isWhitelisted(address user)
public view returns (bool) {
for (uint256 i; i < whitelist.length; ++i) {
if (whitelist[i] == user) return true;
}
return false;
}
// AFTER — O(1) lookup, always cheap
mapping(address => bool) public whitelist;
function isWhitelisted(address user)
public view returns (bool) {
return whitelist[user]; // single SLOAD
}
Quick Reference — Gas Savings Summary
| Pattern | Gas Saved | Difficulty |
|---|---|---|
| 1. Cache storage reads | ~800 per SLOAD | Easy |
| 2. Custom errors | ~50 per revert | Easy |
| 3. Immutable variables | ~797 per read | Easy |
| 4. Storage packing | Up to 15,000 | Medium |
| 5. ++i over i++ | ~5 per iteration | Easy |
| 6. Cache array length | ~100 per iteration | Easy |
| 7. Unchecked arithmetic | ~25 per operation | Medium |
| 8. calldata over memory | ~200-500 per call | Easy |
| 9. external over public | ~10-50 per call | Easy |
| 10. Short-circuit logic | Varies | Easy |
| 11. No redundant storage | 20,000 per slot | Medium |
| 12. Events over storage | ~18,000 per record | Medium |
| 13. Avoid zero writes | Up to 15,000 | Medium |
| 14. Batch operations | 21,000 per tx | Medium |
| 15. Mappings over arrays | O(n) → O(1) | Easy |
How to Apply All 15 Patterns Automatically
Reviewing a contract manually for all 15 patterns takes hours. Blockhertz AI Gas Optimizer applies all patterns automatically in under 60 seconds.
How it works:
- Go to blockhertz.com/tools/gas-optimizer
- Paste your Solidity contract
- Click Optimize Gas
- Get back a fully optimized contract with every changed line marked
// GAS-OPT - See estimated gas savings percentage
- Download the optimized
.solfile
No signup required for the free tier. No credit card needed.
Try it free: blockhertz.com/tools/gas-optimizer
Manual vs AI Gas Optimization
| Manual Review | AI Optimizer | |
|---|---|---|
| Time | 2-8 hours | Under 60 seconds |
| Cost | $500-2,000 | Free |
| Patterns covered | Depends on reviewer | All 15 patterns |
| Output | Report only | Optimized contract + report |
| Best for | Complex logic review | Pattern optimization |
Recommended approach:
- Run AI Gas Optimizer first — apply all automatic pattern optimizations
- Review the GAS-OPT annotations to understand each change
- Run your full test suite to verify behavior is unchanged
- For complex protocols, follow up with manual review of business logic
Conclusion
Gas optimization is one of the highest-ROI activities in smart contract development. The 15 patterns in this guide are not theoretical — they are battle-tested techniques used by production DeFi protocols handling billions in value.
Start with the easiest wins: cache storage reads, use custom errors, and declare immutable variables. These three patterns alone can reduce gas costs by 20-40% in most contracts with minimal code changes.
For a complete automated analysis of your specific contract, run it through the free Blockhertz AI Gas Optimizer. You'll get a fully optimized version with every change explained — in under 60 seconds.
Want to automatically apply all 15 patterns to your contract? Try the free Blockhertz AI Gas Optimizer — results in under 60 seconds, no signup required.
Related Blockhertz Tools
Views
6
Read Time
10 min read
Likes
0
Published
Aug 3, 2026
SIGNAL THREAD
NO SIGNALS YET — BE FIRST TO TRANSMIT
Technical Writer Team Blockhertz
Blockchain & Web3 Innovator
Blockhertz is a collective of blockchain developers, architects, and innovators dedicated to building next-gen Web3 solutions. Our team specialises in DeFi, tokenomics, smart contracts, and distributed systems.