# How to Write Gas-Efficient Loops in Solidity
Loops are gas-intensive operations in Solidity. Every iteration costs gas, and inefficient loop patterns can drain users' wallets. This guide covers proven optimization techniques.
The Problem with Naive Loops
Consider this common pattern:
contract BadLoop {
address[] public users;
function sumBalances() public view returns (uint256) {
uint256 total = 0;
for (uint256 i = 0; i < users.length; i++) {
total += address(users[i]).balance;
}
return total;
}
}
This code reads users.length from storage on every iteration — extremely wasteful.
Optimization 1: Cache Array Length
function sumBalancesOptimized() public view returns (uint256) {
uint256 total = 0;
uint256 len = users.length; // Cache length
for (uint256 i = 0; i < len; i++) {
total += address(users[i]).balance;
}
return total;
}
Savings: ~2,100 gas per iteration for storage reads.
Optimization 2: Unchecked Arithmetic
function sumBalancesCheaper() public view returns (uint256) {
uint256 total = 0;
uint256 len = users.length;
for (uint256 i = 0; i < len;) {
total += address(users[i]).balance;
unchecked { ++i; }
}
return total;
}
Since i can't overflow in reasonable array sizes, skip overflow checks.
Savings: ~40 gas per iteration.
Optimization 3: Use ++i Instead of i++
// Costs more (creates temporary copy)
for (uint256 i = 0; i < len; i++) { }
// Costs less (increments directly)
for (uint256 i = 0; i < len; ++i) { }
Savings: ~5 gas per iteration.
Optimization 4: Avoid Storage Writes in Loops
// BAD: Writes to storage every iteration
mapping(address => uint256) public scores;
function badUpdate(address[] calldata addrs) external {
for (uint256 i = 0; i < addrs.length; i++) {
scores[addrs[i]]++; // 5,000+ gas per write
}
}
// GOOD: Batch updates when possible
function goodUpdate(address[] calldata addrs, uint256[] calldata amounts) external {
require(addrs.length == amounts.length, "Length mismatch");
for (uint256 i = 0; i < addrs.length;) {
scores[addrs[i]] = amounts[i]; // Single write per user
unchecked { ++i; }
}
}
Optimization 5: Early Exit Conditions
function findUser(address target) public view returns (bool) {
uint256 len = users.length;
for (uint256 i = 0; i < len;) {
if (users[i] == target) return true; // Exit early
unchecked { ++i; }
}
return false;
}
Full Optimized Pattern
contract OptimizedLoop {
address[] public users;
function processUsers() external {
uint256 len = users.length;
address user;
for (uint256 i = 0; i < len;) {
user = users[i];
// Process user...
unchecked { ++i; }
}
}
}
When to Avoid Loops Entirely
If unbounded arrays are involved, consider:
- Pagination: Process chunks across multiple transactions
- Pull over Push: Let users claim rewards individually
- Events + Off-Chain: Emit events, aggregate off-chain
Benchmarks
| Pattern | Gas Cost (100 iterations) |
|---------|---------------------------|
| Naive loop | 450,000 |
| Cached length | 240,000 |
| + unchecked | 236,000 |
| + ++i | 235,500 |
Key Takeaways
.length outside loopsunchecked { ++i; } for countersforge snapshotLoops are unavoidable, but smart optimization can save thousands of gas per transaction.