Gas Optimization in Solidity: Practical Techniques to Reduce Ethereum Transaction Costs

Gas optimization in Solidity is the practice of reducing the gas consumed by contract deployment and execution so users pay less and protocols can scale more efficiently. This matters on Ethereum L1 where fees spike during demand surges, and it also matters on L2s because the same EVM bytecode carries largely the same gas semantics even when the price per gas unit is lower. For rollups, execution efficiency and minimizing calldata and storage also reduce data posting costs and improve throughput.
This guide focuses on practical, audit-friendly techniques you can apply today, with an emphasis on changes introduced in Solidity 0.8.x, modern tooling, and patterns used across DeFi, NFT, and infrastructure projects.

Why Gas Optimization in Solidity Matters in 2026
Ethereum's fee market still rewards efficient contracts. Even with EIP-1559 smoothing some dynamics, users compete for inclusion by bidding priority fees during congestion. If your protocol handles high-volume flows such as swaps, liquidations, mints, or account operations, small inefficiencies compound into real costs and degraded user experience.
On L2s, teams sometimes assume gas is cheap enough to ignore. In practice, efficient execution still improves:
- End-user costs for frequent actions like deposits, swaps, and transfers.
- Protocol throughput under sequencer limits.
- Rollup data efficiency because calldata and state updates affect compression and L1 posting costs.
Professional audits increasingly include gas findings on hot paths, and teams now adopt gas budgets in CI pipelines similarly to performance budgets in traditional software engineering.
Know What Is Expensive: Storage, Calldata, and Deployment
The biggest wins typically come from avoiding unnecessary state changes. Storage writes are often the dominant cost:
- SSTORE from zero to non-zero costs 20,000 gas.
- SSTORE from non-zero to non-zero costs 5,000 gas.
- Clearing storage can yield gas refunds, though refunds are capped and should not be treated as a reliable economic assumption.
Calldata carries its own cost, and on rollups it can represent a significant portion of the total fee due to L1 data posting. Deployment size also matters because larger bytecode increases deployment gas and can add overhead to certain patterns.
High-Impact Techniques for Gas Optimization in Solidity
If you implement only a few optimizations, prioritize those that reduce storage operations, minimize data copying, and reduce revert overhead.
1. Minimize Storage and On-Chain Data
Store only what must be read on-chain. If data serves analytics, history, or UI purposes, emitting events is significantly cheaper than persisting the same data in storage. The trade-off is that smart contracts cannot read past logs, so reserve events for off-chain consumption only.
- Use events for audit trails such as status changes and metadata references when on-chain reads are not required.
- Delete state when appropriate using
deleteto clear slots and potentially capture refunds. Apply this carefully because refunds are limited and may change across network upgrades. - Avoid redundant initialization: state variables default to zero, so explicitly setting them to zero in constructors wastes gas.
2. Choose the Right Data Structure
Selecting efficient storage layouts and access patterns can outperform many micro-optimizations.
- Prefer mappings for random access:
mappingprovides O(1) access and avoids array growth overhead. The trade-off is that you cannot iterate mappings on-chain without additional indexing. - Use fixed-size arrays when the size is known: fixed-size arrays avoid dynamic resizing logic and repeated length updates.
3. Pack Storage Slots Where It Matters
Solidity stores state in 32-byte slots. When you use multiple smaller types in a struct, you can often pack them into fewer slots, reducing both SSTORE and SLOAD costs. Common candidates include timestamps and flags:
- Use smaller integer types in storage primarily for packing, such as
uint64for timestamps anduint128for bounded values. - Group variables by size within structs to help the compiler pack them efficiently.
- Do not optimize away readability. When narrowing types, use safe casting patterns and clear invariants to keep audits tractable.
4. Use Calldata for External Read-Only Parameters
For external functions, passing arrays, strings, and bytes as calldata avoids copying into memory. This is one of the most reliable, low-risk optimizations in modern Solidity.
- Use
function foo(uint256[] calldata amounts)for read-only external inputs. - Avoid converting to
memoryunless you need to mutate the data.
5. Cache Storage Reads and Write Back Once
Repeated reads from storage are much more expensive than using local variables. A sound pattern is:
- Read storage once into a local variable.
- Perform computations in memory.
- Write back only if needed.
This approach is particularly impactful in swaps, accounting updates, position management, and batch operations.
6. Prefer Constants and Immutables for Configuration
Constants and immutables eliminate recurring storage reads. Constants are embedded directly into bytecode and inlined by the compiler at runtime. Immutables are set once at deployment and handled efficiently by the compiler thereafter.
- Use
uint256 public constant FEE_BPS = 30;for compile-time values. - Use
address public immutable treasury;for deployment-time configuration.
7. Use Custom Errors Instead of Revert Strings
Solidity 0.8.4 introduced custom errors, which are more compact than revert strings. They reduce deployment size and lower runtime gas when reverts occur. This is particularly valuable in flows where reverts are common, such as slippage checks, allowlist checks, and collateral validations.
Example pattern:
error InsufficientCollateral(address user, uint256 needed, uint256 actual);if (actual < needed) revert InsufficientCollateral(msg.sender, needed, actual);
If you must use revert strings, keep them short and consistent.
8. Apply Unchecked Arithmetic Only with Proven Bounds
Solidity 0.8.x adds overflow checks by default, improving safety at the cost of some gas overhead. The unchecked { ... } block skips these checks. Use it only when you can prove safety, such as bounded loop counters or carefully constrained math.
- Good candidate: incrementing a loop index that cannot reach
type(uint256).max. - Poor candidate: user-influenced arithmetic in financial calculations without strict bounds.
9. Optimize Loops and Control Flow
Loop optimizations can be meaningful in batch mints, batch transfers, and accounting updates.
- Cache array length in a local variable instead of reading it on every iteration.
- Avoid unbounded loops in user-facing functions to prevent block gas limit failures.
- Use short-circuiting (
&&,||) so the expensive side evaluates less often when early exits are common.
Micro-optimizations such as favoring certain comparison operators or using bit shifts for multiplication and division by powers of two exist but should be treated as last-mile improvements reserved for tight loops or extremely high-frequency paths.
Compiler and Deployment Strategies That Move the Needle
Enable the Optimizer and Tune the Runs Setting
For production deployments, enabling the compiler optimizer is standard practice. The --runs setting is a key configuration knob:
- Higher runs tends to reduce runtime gas for frequently called contracts, often at the cost of slightly larger deployment bytecode.
- Lower runs can reduce deployment size, which may suit contracts that are deployed often but called rarely.
Choose the runs value based on expected usage patterns and validate decisions using gas reports on representative workloads.
Use Minimal Proxy Clones for Factories
If you deploy many similar contract instances, EIP-1167 minimal proxies can reduce deployment cost substantially by delegating calls to a shared implementation. This pattern is common in vault factories, smart account factories, and modular DeFi systems.
Inline Assembly and Yul, Selectively
Inline assembly can remove overhead and target EVM opcodes directly, but it increases audit complexity and bypasses safety checks. Reserve it for performance-critical, well-tested components and keep the surface area as small as possible.
A Practical Workflow: Measure, Optimize, and Guard Against Regressions
Gas optimization in Solidity should be evidence-driven. A reliable workflow looks like this:
- Identify hot paths: functions called frequently or in batches, and functions on critical user journeys.
- Benchmark using Hardhat or Foundry gas reporting and capture baseline numbers before making changes.
- Apply major wins first: reduce storage writes, switch to calldata, add caching, adopt custom errors, and pack key structs.
- Add CI guardrails: set gas thresholds for key functions and fail builds on regressions.
- Re-audit assumptions after refactors, especially when using unchecked blocks or packed storage layouts.
For teams building skills systematically, consider internal training aligned with recognized curricula. Blockchain Council certifications such as the Certified Solidity Developer and Certified Smart Contract Auditor programs cover EVM internals, gas modeling, and secure optimization practices.
Real-World Examples Where Optimizations Pay Off
- DeFi: packed position structs, caching reserves during swaps, and custom errors on revert-heavy validation paths.
- NFTs: loop tuning for batch mints and transfers, minimizing storage writes for metadata changes, and using events where on-chain reads are unnecessary.
- On-chain games: bitmaps and bitmasks for flags, keeping heavy state off-chain, and using compact on-chain references.
- Account abstraction: minimal proxy clones for smart accounts and careful calldata sizing for user operations.
- DAOs: event-based logging for proposal details with on-chain commitments rather than storing everything in state.
Conclusion: Optimize Where It Matters, Without Compromising Security
Gas optimization in Solidity is a core engineering competency. The best results come from design-level choices: minimizing storage, controlling data size, selecting efficient structures, and leveraging modern Solidity features like custom errors and calldata parameters. Reserve micro-optimizations for after profiling confirms a function is genuinely on the hot path.
Most importantly, never trade correctness for marginal savings. Aggressive packing, unchecked arithmetic, and low-level code can introduce vulnerabilities that far outweigh any fee reduction. Measure changes, document invariants, and treat gas work as an integral part of secure engineering practice.
Related Articles
View AllSolidity
Solidity Design Patterns for Web3 Apps: Access Control, Pausability, and Modular Architecture
Learn Solidity design patterns for access control, pausability, and modular architecture, including RBAC, governance admin, and upgrade-safe Web3 contract design.
Solidity
Writing ERC-20 and ERC-721 Tokens in Solidity: Standards, Extensions, and Real-World Pitfalls
Learn how to write ERC-20 and ERC-721 tokens in Solidity with key extensions like permit and royalties, plus real-world pitfalls in security, gas, UX, and compliance.
Solidity
Solidity Events and Logs Explained: Building Indexable On-Chain Analytics for dApps
Learn how Solidity events and logs work, how indexed topics enable fast off-chain queries, and how to design event schemas for scalable dApp analytics.
Trending Articles
The Role of Blockchain in Ethical AI Development
How blockchain technology is being used to promote transparency and accountability in artificial intelligence systems.
What is AWS? A Beginner's Guide to Cloud Computing
Everything you need to know about Amazon Web Services, cloud computing fundamentals, and career opportunities.
Can DeFi 2.0 Bridge the Gap Between Traditional and Decentralized Finance?
The next generation of DeFi protocols aims to connect traditional banking with decentralized finance ecosystems.