Gas Optimization in Solidity: Complete Guide, Examples, Risks and Best Practices
Gas optimization in Solidity means writing smart contracts that use fewer EVM resources while still staying secure, readable, and correct. On Ethereum and EVM-compatible networks, every transaction needs gas. Users pay for computation, storage, contract deployment, and data included in the transaction. If a contract wastes gas, users pay more and the application may become expensive to use during busy network periods.
For beginners, gas optimization can sound like advanced magic. It is not. Most useful optimizations come from a few practical ideas: store less data on-chain, avoid unnecessary storage writes, use the right data location, keep loops under control, enable the Solidity compiler optimizer, and measure changes before assuming they help.
The goal is not to make every line as short as possible. The real goal is to reduce meaningful costs without creating security bugs, confusing future developers, or breaking expected behavior.

Diagram: A practical gas optimization workflow for Solidity projects.
1. What Is Gas in Solidity?
Gas is the unit used to measure the computational work performed by the Ethereum Virtual Machine, often called the EVM. Solidity code is compiled into EVM bytecode. When someone calls a function, the EVM executes low-level operations called opcodes. Some opcodes are cheap, such as simple arithmetic or memory reads. Others are more expensive, especially persistent storage operations and contract creation.
A user does not pay gas directly as a standalone token. They pay a transaction fee based on gas used multiplied by the gas price or fee rules of the network. On Ethereum after EIP-1559, transactions include a base fee and priority fee, while many layer 2 networks have their own fee models. The Solidity optimization principles are still useful because fewer operations generally mean cheaper execution, but exact fees vary by chain and network conditions.
1.1 Gas used vs gas price
| Term | Meaning | What developers can control |
|---|---|---|
| Gas used | How much EVM work a transaction consumes. | Mostly yes. Contract design and code affect gas used. |
| Gas price or transaction fee rate | How much the user pays per unit of gas. | Mostly no. It depends on network demand, chain fee rules, and wallet settings. |
| Total transaction fee | Gas used multiplied by the applicable fee rate, plus network-specific costs. | Partly. Developers reduce gas used; users and networks determine the fee rate. |
2. Why Gas Optimization Matters
Gas optimization matters because smart contract users pay for inefficient code every time they interact with it. A small waste in a rarely used admin function may not matter much. The same waste inside a high-volume swap, mint, claim, transfer, staking, or voting function can become expensive for thousands of users.
- Better user experience: cheaper transactions reduce friction and failed transactions.
- Higher scalability: efficient contracts fit more work into block gas limits and layer 2 execution limits.
- Lower deployment cost: smaller and simpler bytecode can reduce the cost of deploying contracts.
- More competitive products: in DeFi, gaming, NFTs, and consumer apps, transaction cost affects adoption.
- Cleaner engineering: optimization often reveals unnecessary state, duplicated logic, and poor data design.
However, gas optimization should never be used as an excuse to weaken security. A $5 gas saving is not worth a bug that can lock or drain funds.
3. The Most Important Rule: Measure Before and After
Many gas tips are context-dependent. A change that saves gas in one contract may increase gas in another because of compiler behavior, storage layout, calldata size, optimizer settings, inheritance, or how often a function is called. Good optimization starts with measurement.
3.1 Useful tools for measuring gas
| Tool | Useful for | Beginner note |
|---|---|---|
| Foundry gas reports | Measuring test-level and function-level gas usage. | Run tests with gas reporting enabled and compare commits. |
| Hardhat Gas Reporter | Estimating gas usage inside a Hardhat test suite. | Helpful for teams already using Hardhat. |
| Remix gas estimates | Quick learning and simple examples. | Good for beginners, but not enough for production decisions. |
| Block explorers | Checking actual gas used by deployed transactions. | Use real transactions to validate assumptions. |
| Solidity compiler output | Bytecode size and estimated gas. | Useful, but estimates may not match all runtime paths. |
3.2 A simple measurement workflow
- Write a test that covers the function you want to optimize.
- Record the gas before changing code.
- Make one optimization at a time.
- Run the same test again.
- Keep the change only if it saves meaningful gas without reducing clarity or safety.
- Add regression tests so future edits do not accidentally increase gas in critical functions.
4. Where Gas Costs Usually Come From
Not all Solidity operations cost the same. Beginners often focus on tiny syntax changes, but the largest savings usually come from design and storage choices.
| Cost area | Why it matters | Optimization idea |
|---|---|---|
| Persistent storage | Writing data to contract storage is among the most expensive common operations. | Store only what the contract must remember permanently. |
| Contract deployment | Large bytecode costs more to deploy and may approach size limits. | Remove unused logic and use libraries carefully. |
| Loops | A loop can become too expensive as arrays grow. | Bound loop sizes or use pull-based designs. |
| External calls | Calls to other contracts add overhead and security risk. | Batch carefully, cache results, and avoid unnecessary calls. |
| Calldata size | Large inputs and return data increase transaction cost. | Pass compact data and avoid unnecessary dynamic data. |
| Events and logs | Events cost gas, but are often cheaper than permanent storage for off-chain history. | Emit events for history; store only state needed by contracts. |
5. Beginner-Friendly EVM Data Locations
Solidity has different places where data can live. Choosing the right location is one of the easiest ways to avoid wasted gas.
| Data location | Persists after function? | Can be modified? | Typical use |
|---|---|---|---|
| storage | Yes | Yes | State variables such as balances, ownership, configuration, mappings. |
| memory | No | Yes | Temporary arrays, structs, strings, and calculations inside a function. |
| calldata | No | No | Read-only external function parameters, especially arrays and strings. |
| transient storage | Only during current transaction | Yes | Advanced use cases such as cheaper transaction-scoped reentrancy locks on Cancun-compatible EVMs. |
As a beginner rule, use storage only when the contract must remember data after the transaction. Use calldata for read-only external function parameters. Use memory when you need a temporary, modifiable copy.
6. Gas Optimization Examples in Solidity
6.1 Example 1: Use calldata for read-only external inputs
When an external function receives an array or string that it only reads, calldata can avoid copying the input into memory.
// Less efficient when the array is only read
function sum(uint256[] memory numbers) external pure returns (uint256 total) {
for (uint256 i = 0; i < numbers.length; i++) {
total += numbers[i];
}
}
// Usually better for read-only external input
function sum(uint256[] calldata numbers) external pure returns (uint256 total) {
for (uint256 i = 0; i < numbers.length; i++) {
total += numbers[i];
}
}
Practical advice: Prefer calldata for external function parameters that are arrays, structs, bytes, or strings and are not modified.
6.2 Example 2: Cache storage reads in a local variable
Reading from storage repeatedly can be wasteful. If you need the same storage value multiple times, copy it into a local variable and use the local variable.
contract RewardVault {
uint256 public rewardRate;
// Less efficient: reads rewardRate from storage repeatedly
function quoteA(uint256 amount) external view returns (uint256) {
return amount * rewardRate + rewardRate;
}
// Better: one storage read, then use memory/stack value
function quoteB(uint256 amount) external view returns (uint256) {
uint256 rate = rewardRate;
return amount * rate + rate;
}
}
Do not overuse this for values read only once. It helps most when the same storage value is used multiple times in one function.
6.3 Example 3: Reduce storage writes
Writing to storage is often more expensive than reading. Avoid writing a value if it is already correct.
contract Settings {
uint256 public feeBps;
error SameFee();
function setFee(uint256 newFeeBps) external {
if (newFeeBps == feeBps) revert SameFee();
feeBps = newFeeBps;
}
}
This pattern is useful in admin functions and configuration updates. It also creates a clearer signal for users and indexers because state-changing transactions only happen when state changes.
6.4 Example 4: Pack storage variables carefully
Solidity stores state variables in 32-byte slots. Smaller value types can sometimes share one slot if they are declared next to each other. Packing can reduce storage slots and storage operations.
// Less efficient layout: small values are separated by a full uint256
contract BadLayout {
uint128 amount;
uint256 createdAt;
uint128 claimed;
}
// Better layout: the two uint128 values can share one 32-byte slot
contract BetterLayout {
uint128 amount;
uint128 claimed;
uint256 createdAt;
}
Warning: Packing is not always automatically better. If you frequently update only one packed value, the EVM may need to read and rewrite the slot carefully. Measure before deciding.
6.5 Example 5: Use custom errors instead of long revert strings
Custom errors usually reduce deployment size and can reduce revert cost compared with long revert strings.
// More bytecode because the revert string is stored in the contract
require(msg.sender == owner, "Only the owner can call this function");
// More gas-efficient and easier to standardize
error NotOwner(address caller);
if (msg.sender != owner) {
revert NotOwner(msg.sender);
}
Custom errors are especially useful in production contracts with many checks. Keep error names clear so developers and frontends can understand failures.
6.6 Example 6: Prefer constants and immutables for values that do not change
If a value is known at compile time, use constant. If it is known at deployment time and never changes, use immutable. This can reduce storage reads because the value does not need to be loaded from persistent storage like a normal state variable.
contract TokenSale {
uint256 public constant MAX_BPS = 10_000;
address public immutable treasury;
constructor(address _treasury) {
treasury = _treasury;
}
}
Use normal storage only for values that must change after deployment.
6.7 Example 7: Avoid unbounded loops over growing arrays
A function that loops over an array that grows forever can eventually become too expensive to call. This is a common beginner mistake.
// Risky: if users becomes very large, this may run out of gas
function payEveryone() external {
for (uint256 i = 0; i < users.length; i++) {
_pay(users[i]);
}
}
// Safer design: each user claims their own payment
function claim() external {
uint256 amount = claimable[msg.sender];
claimable[msg.sender] = 0;
payable(msg.sender).transfer(amount);
}
This is more than a gas issue. Unbounded loops can create denial-of-service risks because an important function may become impossible to execute.
7. High-Impact Gas Optimization Techniques
7.1 Optimize the contract design before optimizing syntax
The biggest savings usually come from asking better design questions: Does this data need to be on-chain? Does every user need a separate storage write? Can users pull funds instead of the contract pushing funds to everyone? Can historical information be emitted as events instead of stored permanently?
- Store final state on-chain, not every intermediate calculation.
- Use events for off-chain history and analytics when contracts do not need to read the data.
- Avoid storing duplicate data that can be derived from existing state.
- Use mappings for direct lookup instead of scanning arrays.
- Design claims, withdrawals, and reward distribution as user-triggered actions when possible.
7.2 Minimize persistent storage
Persistent storage is expensive because it changes blockchain state that must be maintained by nodes. Before adding a state variable, ask whether the contract needs it later. If only a frontend, backend, or analytics tool needs it, an event may be enough.
| Need | Better choice | Reason |
|---|---|---|
| Contract must enforce future rules | storage | The contract needs to read the value later. |
| Frontend needs transaction history | event | Events are easier for off-chain indexing and avoid permanent state bloat. |
| Temporary calculation | memory or stack | No need to persist the value. |
| Read-only function input | calldata | Avoid unnecessary memory copy. |
7.3 Batch carefully
Batching can save overhead by combining many actions into one transaction. But batching can also create large transactions that fail or become expensive. Use batching when the number of operations is bounded and predictable. Avoid batching that depends on an array that can grow without limit.
7.4 Use mappings for direct access
Arrays are useful for ordered lists, but searching through arrays costs gas proportional to the array size. If you need to check whether a user is approved, has claimed, or owns a record, a mapping is usually better.
mapping(address => bool) public hasClaimed;
function claim() external {
if (hasClaimed[msg.sender]) revert AlreadyClaimed();
hasClaimed[msg.sender] = true;
// send or mint reward
}
7.5 Keep critical functions small and focused
Functions called frequently should do only the work needed for that user action. Move optional analytics, rare admin tasks, and complex views off the hot path when possible. A clean contract architecture often reduces gas more safely than micro-optimizations.
7.6 Enable and tune the Solidity optimizer
The Solidity compiler optimizer can reduce bytecode size and runtime gas. Most production projects enable it. The optimizer runs setting is a trade-off: lower runs may favor cheaper deployment, while higher runs may favor cheaper repeated execution. There is no universal best number. Common defaults such as 200 are only a starting point.
// Example Hardhat setting
solidity: {
version: "0.8.28",
settings: {
optimizer: {
enabled: true,
runs: 200
},
viaIR: true
}
}
Practical advice: Benchmark your real functions with realistic tests. If the contract will be called many times, runtime savings may matter more than deployment cost. If it is deployed many times but rarely called, bytecode size and deployment cost may matter more.
7.7 Consider viaIR, but test thoroughly
The IR-based compilation pipeline can sometimes produce better optimized bytecode and help avoid stack-too-deep issues. It can also change compilation behavior and build time. Use it deliberately, test thoroughly, and keep compiler versions pinned for reproducible deployments.
7.8 Use libraries and imports thoughtfully
Reliable libraries such as OpenZeppelin Contracts can reduce security risk and development time. But importing large modules for a tiny feature may increase bytecode size. Prefer well-reviewed libraries for security-critical standards, and remove unused code from custom contracts.
7.9 Use unchecked arithmetic only when clearly safe
Solidity 0.8 and later checks arithmetic overflow and underflow by default. In tight loops, developers sometimes use unchecked increments when they can prove overflow is impossible.
for (uint256 i = 0; i < length; ) {
// work
unchecked { i++; }
}
This is an advanced micro-optimization. Use it only when the bound is clear, tests cover it, and the code remains easy to audit.
7.10 Delete storage when it is genuinely no longer needed
Deleting storage can reduce state and may affect gas accounting, but refunds are limited and have changed across Ethereum upgrades. Do not design business logic around large refunds. Delete data because it is correct and keeps state clean, not because you expect a guaranteed large refund.
8. Gas Optimization Techniques Comparison
| Technique | Potential impact | Risk level | Best used when |
|---|---|---|---|
| Reduce storage writes | High | Low to medium | A function writes state often or writes duplicate values. |
| Use calldata | Medium | Low | External parameters are read-only dynamic types. |
| Pack storage variables | Medium to high | Medium | Several small values are stored and usually read/written together. |
| Custom errors | Low to medium | Low | Contracts have many require/revert messages. |
| Compiler optimizer | Medium to high | Medium | Production deployment with a test suite and pinned compiler. |
| Unbounded loop redesign | High | Low | A function loops through user-controlled or growing arrays. |
| Assembly | Variable | High | Only after profiling and expert review. |
| Transient storage | Medium | Medium to high | Transaction-scoped data on compatible EVM versions, such as locks. |
9. Risks and Mistakes in Solidity Gas Optimization
9.1 Mistake 1: Optimizing before the contract is correct
A contract should first be secure and correct. Optimizing broken logic only makes a cheaper broken contract. Write tests, define invariants, and confirm expected behavior before chasing gas savings.
9.2 Mistake 2: Sacrificing readability for tiny savings
Smart contracts are high-risk software. If a future auditor or maintainer cannot understand a function, the contract becomes more dangerous. A tiny gas saving is rarely worth unclear code.
9.3 Mistake 3: Using assembly without a strong reason
Inline assembly can bypass Solidity safety checks and make code harder to audit. Use it only when high-level Solidity cannot achieve a necessary result, and document exactly why it is safe.
9.4 Mistake 4: Creating denial-of-service risks with loops
Loops over arrays that grow with users can become impossible to execute. This can freeze payments, rewards, governance actions, or admin operations. Prefer pull-based designs, pagination, or bounded loops.
9.5 Mistake 5: Assuming all chains price gas the same way
Ethereum mainnet, optimistic rollups, zero-knowledge rollups, sidechains, and appchains can price execution, calldata, and storage differently. Optimize for the chain where users will actually transact.
9.6 Mistake 6: Ignoring upgradeable contract storage layout
If you use proxies or upgradeable contracts, changing storage layout can break existing state. Storage packing and variable reordering are dangerous after deployment. For upgradeable systems, follow strict storage layout rules and use upgrade safety tooling.
10. Best Practices Checklist
| Area | Best practice |
|---|---|
| Design | Avoid unnecessary on-chain state; use events for historical data. |
| Storage | Minimize writes, cache repeated reads, and pack variables only when appropriate. |
| Inputs | Use calldata for read-only external dynamic parameters. |
| Loops | Avoid unbounded loops over growing arrays; use mappings, pull patterns, or pagination. |
| Errors | Use custom errors instead of long revert strings. |
| Compiler | Enable optimizer, pin compiler version, and benchmark optimizer runs. |
| Testing | Use gas reports, realistic tests, fuzzing, and regression thresholds. |
| Security | Never trade clear safety checks for small gas savings. |
| Deployment | Check bytecode size, constructor cost, and network-specific fee behavior. |
| Documentation | Explain non-obvious optimizations in comments for auditors and maintainers. |
11. Practical Gas Optimization Workflow for a New Solidity Project
- Start with a simple, secure contract design.
- Decide what must be stored on-chain and what can be emitted as events.
- Write tests for normal behavior, failure cases, and edge cases.
- Enable the Solidity optimizer in a development branch.
- Run gas reports for critical user functions.
- Optimize high-cost functions first, especially repeated storage writes and loops.
- Compare gas before and after each change.
- Run security tests again after optimization.
- Review code readability and add comments for non-obvious choices.
- Before deployment, freeze compiler settings and verify the contract source.
12. Real-World Scenario: NFT Mint Function
Imagine an NFT collection where thousands of users call mint(). Gas optimization matters because minting is a high-volume user action. A good mint function should avoid unnecessary storage writes, avoid loops over all minters, use custom errors, and keep checks simple.
12.1 Less efficient pattern
address[] public minters;
function mint(uint256 quantity) external payable {
require(quantity > 0, "Quantity must be greater than zero");
require(msg.value == quantity * price, "Wrong payment amount");
minters.push(msg.sender);
for (uint256 i = 0; i < quantity; i++) {
_mint(msg.sender, totalSupply() + 1);
}
}
12.2 More efficient thinking
- Use a custom error for invalid quantity and payment mismatch.
- Avoid storing minters unless the contract needs that array on-chain.
- If off-chain analytics need minter history, emit an event instead.
- Avoid loops with large quantity values by setting a maximum quantity per transaction.
- Cache price and supply values if used repeatedly.
The exact implementation depends on the NFT standard and library used. The key lesson is to optimize the hot path that users call frequently, not rare admin functions first.
13. When Not to Optimize
Sometimes the best decision is to leave code alone. Do not optimize when the function is rarely used, the saving is tiny, the change makes the code harder to audit, or the optimization relies on assumptions you cannot prove. In smart contracts, clarity and safety are part of performance because secure code is less likely to need emergency fixes, migrations, or user compensation.
14. Beginner Glossary
| Term | Simple meaning |
|---|---|
| EVM | The Ethereum Virtual Machine that executes smart contract bytecode. |
| Opcode | A low-level EVM instruction such as ADD, SLOAD, or SSTORE. |
| SLOAD | The EVM operation for reading from persistent storage. |
| SSTORE | The EVM operation for writing to persistent storage. |
| Calldata | Read-only input data for external calls. |
| Memory | Temporary modifiable data during a function call. |
| Storage slot | A 32-byte location where contract state is stored. |
| Optimizer runs | A compiler setting that influences deployment size vs repeated runtime efficiency. |
| Bytecode | The compiled code deployed to the blockchain. |
| Gas report | A report showing how much gas functions or tests used. |
15. FAQs About Gas Optimization in Solidity
15.1 What is gas optimization in Solidity?
Gas optimization in Solidity is the process of reducing the amount of gas a smart contract uses during deployment and function execution while keeping the contract secure and correct.
15.2 What is the fastest way to reduce gas costs?
The fastest high-impact method is usually reducing unnecessary storage writes and redesigning functions that loop over growing arrays. Compiler settings and syntax improvements help, but design choices usually matter more.
15.3 Should beginners use inline assembly for gas optimization?
Usually no. Inline assembly is harder to read and can bypass Solidity safety features. Beginners should focus on storage, calldata, loops, custom errors, and measurement first.
15.4 Does using calldata always save gas?
No. Calldata is best for read-only external dynamic parameters. If you need to modify the data, you may need memory. Always test the actual function.
15.5 Are custom errors better than require strings?
Custom errors are usually more gas-efficient than long revert strings and make errors easier for frontends to decode. They are a good default for production contracts.
15.6 Is storage packing always good?
No. Packing can reduce storage slots, but it can add read-modify-write overhead when updating one value inside a packed slot. It is best when packed values are commonly used together.
15.7 What optimizer runs value should I use?
There is no universal answer. Many projects start with 200, then benchmark. Higher runs can favor repeated function calls, while lower runs may reduce deployment size. Use realistic tests to decide.
15.8 Can gas optimization make a contract less secure?
Yes. Removing checks, using unsafe arithmetic, adding assembly, or making code hard to understand can introduce vulnerabilities. Security should come before small gas savings.
15.9 Do layer 2 networks make gas optimization unnecessary?
No. Layer 2 transactions are often cheaper than Ethereum mainnet, but inefficient contracts still cost users more and can hit execution or data limits. The best techniques remain useful.
15.10 How do I know whether an optimization is worth it?
Measure the gas saved, estimate how often the function will be called, and compare the saving with the added complexity and risk. Keep optimizations that provide meaningful savings without hurting security or readability.
16. Conclusion
Gas optimization in Solidity is about making smart contracts efficient, not clever for its own sake. The best improvements usually come from better contract design: less permanent storage, fewer unnecessary writes, bounded loops, calldata for read-only inputs, custom errors, and careful compiler settings. Advanced techniques such as transient storage, viaIR, and assembly can help in specific cases, but they require stronger testing and review.
A practical rule for every Solidity developer is simple: write secure code first, measure gas second, optimize the expensive paths third, and keep the final contract understandable enough for auditors, maintainers, and future you.
Sources Consulted and Checked
These sources were consulted and checked while preparing this article and reviewing its technical accuracy.
- Solidity documentation: Contracts and transient storage
- Solidity documentation: State variable storage layout
- Solidity documentation: Using the compiler
- Ethereum.org: EVM opcodes reference
- OpenZeppelin Forum: A collection of gas optimisation tricks
Reader Advice
This article is provided for educational and informational purposes and is not personalized legal, financial, security, or deployment advice. Smart-contract development and gas optimization involve technical and financial risks, including coding errors, security vulnerabilities, failed transactions, unexpected fees, incompatibility across networks, and loss of digital assets. Rules, policies, laws, compiler behavior, network fee models, and statistics can change over time and vary by jurisdiction and blockchain. Before making decisions or deploying code, verify current information through official documentation and qualified professionals, test thoroughly in an appropriate environment, obtain an independent security review where warranted, and use only funds and systems you can afford to place at risk.