DEV Community

Cover image for Gas Inefficiencies Developers Don't Notice Until It's Too Late.
Progress Ochuko Eyaadah
Progress Ochuko Eyaadah

Posted on

Gas Inefficiencies Developers Don't Notice Until It's Too Late.

In May 2023, a DeFi protocol launched its token claim contract. Within 48 hours, users had spent over $2 million in gas fees, not because the contract was complex, but because the developers hadn't optimized their loops. Each claim processed the same storage array 100+ times instead of caching it once.

The contract worked. It just costs users 10x more than it should have. This wasn't a hack. It wasn't a bug. It was inefficient code that silently compounded until real money started burning.

Here's the reality: Compute and storage are never free. On Ethereum, every opcode has a gas price. On Stellar's Soroban, computation and storage consume transaction budgets. Solana charges compute units. Polkadot weighs transactions. Different execution models, same outcome, inefficient code becomes a production problem fast.

If you're building on any blockchain, gas optimization isn't optional. It's the difference between a protocol users can afford to use and one they abandon for cheaper alternatives.

Why Gas Costs Soar Out of Control

The biggest culprit across all chains? Storage operations.

On EVM chains (Ethereum, Polygon, BSC, Arbitrum):

  • mload and mstore (memory): 3 gas per operation.

  • sload and sstore (storage): 100+ gas minimum.

As defined in the Ethereum Yellow Paper, storage operations cost over 100× more than memory operations.

On Non-EVM (Soroban, Solana):

  • Writing to a new storage entry allocates ledger space, consuming significant resources.

  • Storage reads/writes count against your transaction's CPU and memory budget.

  • Cross-program invocations (CPIs) add compute overhead.

The pattern is universal: Touching a persistent state is expensive. The exact mechanism differs by chain, but the principle holds.

The problem isn't just that storage costs more; it's that developers unknowingly trigger these operations repeatedly. A loop that runs 100 times and reads from storage on each iteration? That's 100 expensive reads instead of one cached value, whether you're on EVM or Non-EVM.

Small inefficiencies multiply. And by the time you notice in production, your users are already paying for it.

The 6 Gas Killers (And How to Fix Them)

Most gas bloat comes from patterns developers don't recognize as problems until deployment. These patterns appear across all blockchain architectures; the syntax changes, but the inefficiencies remain the same.

Let's break down the worst offenders with examples across different chains:

1. Unnecessary Storage Writes.

The mistake: Writing default or zero values when they're not needed, or writing data that hasn't actually changed.

Why it's expensive:
EVM chains: Each storage write costs ~20,000 gas. Even after EIP-3529 reduced refunds, writing to an already-zero slot still costs ~2,900 gas on warm slots.

Non-EVM chains (e.g., Soroban): Every storage write consumes part of your transaction budget. Writing unchanged values wastes CPU instructions and ledger I/O.

How to fix it:
On EVM (Solidity):

// ❌ Bad: Always writes
balances[user] = 0;

// ✅ Good: Only writes if needed
if (balances[user] != 0) {
    delete balances[user]; // Triggers gas refund
}
On Non-EVM (Rust/Soroban):

// ❌ Bad: Always writes
env.storage().instance().set(&USER_BALANCE, &0);

// ✅ Good: Check before writing
if env.storage().instance().has(&USER_BALANCE) {
    env.storage().instance().remove(&USER_BALANCE);
}
Enter fullscreen mode Exit fullscreen mode

Additional optimizations:

  • EVM: Pack booleans and small integers into the same 256-bit slot, save entire storage slots (~20,000 gas each).

  • Non-EVM (Soroban): Use temporary storage for data that doesn't need to persist across ledgers.
    Both: Use events/logs instead of storage for infrequently accessed data.

2. Loops That Access Storage Repeatedly

The mistake:Reading from or writing to storage inside loops. Each storage operation costs resources, so if your loop touches storage on every iteration, costs multiply quickly.

Why it's expensive:
A loop that processes 100 items and reads from storage each time equals 100 expensive operations instead of 1 cached read.

How to fix it:
On EVM (Solidity):

// ❌ Bad: Reads storage 100 times
for (uint i = 0; i < users.length; i++) {
    totalBalance += balances[users[i]]; // sload every iteration
}

// ✅ Good: Cache values
uint length = users.length;
uint tempBalance;
for (uint i = 0; i < length; i++) {
    tempBalance += balances[users[i]];
}
totalBalance = tempBalance; // Single sstore
On Non-EVM (Rust/Soroban):

// ❌ Bad: Reads storage in loop
let mut total = 0;
for user in users.iter() {
    total += env.storage().instance().get::<_, i128>(&user).unwrap_or(0);
}


// ✅ Good: Batch reads or cache
let total: i128 = users.iter()
    .map(|u| env.storage().instance().get::<_, i128>(u).unwrap_or(0))
    .sum();
Enter fullscreen mode Exit fullscreen mode

3. Not Validating Inputs Early

The mistake: Performing expensive operations before checking if inputs are even valid.

Why it's expensive:
If a transaction is going to fail anyway, you want it to fail as cheaply as possible. Order matters.

How to fix it:
On EVM (Solidity)

 // ❌ Bad: Expensive check first
require(balances[msg.sender] >= amount); // Storage read
require(amount > 0); // Memory check

// ✅ Good: Cheap checks first
require(amount > 0); // Fails fast if zero
require(balances[msg.sender] >= amount); // Only runs if amount valid
On Non-EVM (Rust/Soroban):

// ❌ Bad: Storage check first
let balance = env.storage().instance().get::<_, i128>(&user).unwrap_or(0);
if amount <= 0 {
    panic!("Invalid amount");
}

// ✅ Good: Validate inputs first
if amount <= 0 {
    panic!("Invalid amount"); // Fails immediately
}
let balance = env.storage().instance().get::<_, i128>(&user).unwrap_or(0);
if balance < amount {
    panic!("Insufficient balance");
}

Enter fullscreen mode Exit fullscreen mode

**Universal principle: **Validate cheaply (bounds checks, null checks) before expensive operations (storage reads, cryptographic operations).

4. Making Multiple Transactions Instead of Batching.

** The mistake:** Splitting work across many transactions instead of grouping operations.

Why it's expensive:

EVM chains:Each transaction has a base fee (~21,000 gas). 100 transactions = 100 base fees.

Non-EVM (eg, Soroban):Each transaction consumes base CPU/memory budget overhead.

How to fix it:
On EVM (Solidity):

// ❌ Bad: 100 transactions = 100 base fees
// User calls transfer() 100 times

// ✅ Good: 1 transaction, 1 base fee
function batchTransfer(address[] calldata recipients, uint[] calldata amounts) external {
    require(recipients.length == amounts.length, "Length mismatch");
    for (uint i = 0; i < recipients.length; i++) {
        _transfer(msg.sender, recipients[i], amounts[i]);
    }
}
Enter fullscreen mode Exit fullscreen mode

On Non-EVM (Soroban/Rust):

// ✅ Good: Batch operations in single contract call
pub fn batch_transfer(
    env: Env,
    from: Address,
    transfers: Vec<(Address, i128)>
) -> Result<(), Error> {
    from.require_auth();

    for (to, amount) in transfers.iter() {
        transfer_internal(&env, &from, to, *amount)?;
    }
    Ok(())
}
Enter fullscreen mode Exit fullscreen mode

5. Inefficient Data Handling

The mistake: Copying data unnecessarily or using the wrong data location for function parameters.

Why it's expensive:
Memory operations and data copies cost resources across all chains.

How to fix it:
On EVM (Solidity):

// ❌ Bad: Copies array to memory
function sum(uint[] memory numbers) public returns (uint) {
    uint total;
    for (uint i = 0; i < numbers.length; i++) {
        total += numbers[i];
    }
    return total;
}

// ✅ Good: Reads directly from calldata
function sum(uint[] calldata numbers) public pure returns (uint) {
    uint total;
    for (uint i = 0; i < numbers.length; i++) {
        total += numbers[i];
    }
    return total;
}
On Non-Evm (Soroban/Rust):

// ❌ Bad: Cloning data unnecessarily
pub fn process_data(env: Env, data: Vec<i128>) -> i128 {
    let copied = data.clone(); // Unnecessary clone
    copied.iter().sum()
}

// ✅ Good: Use references
pub fn process_data(env: Env, data: Vec<i128>) -> i128 {
    data.iter().sum() // No clone needed
}
Enter fullscreen mode Exit fullscreen mode

Additional tips:

EVM (Solidity): Mark read-only functions as view or pure (can be called off-chain for free).
Non-EVM (Rust): Use & references instead of cloning Vec or Map structures.

Your Gas Optimization Checklist

Before you deploy on any chain, run through this checklist:

  • Storage usage:

a. No unnecessary writes to storage.
b. Variables/data packed efficiently.
c. Old entries deleted/removed for refunds (EVM) or budget recovery.
d. Events/logs used instead of storage for infrequently accessed data.

  • Loops:

a. No storage reads/writes inside loops
b. Array lengths and values cached beforehand
c. Heavy computation moved off-chain or batched

  • Data locations:

a. EVM (Solidity): Large inputs use calldata, not memory.
b. Non-Evm (Rust): Use references (&) instead of cloning.

  • Control flow:

a. Cheap validation checks before expensive operations.
b. Conditions structured to short-circuit away from costly paths.

  • Redundant operations:

a. Current values checked before writing.
b. Conditional checks skip no-op writes.
c. State is cleared explicitly when no longer needed.

  • Profiling:

a. Gas/resource reporter integrated into test suite.
b. Functions profiled during development.
c. Static analysis in CI/CD pipeline.

  • Chain-specific optimizations:

a. EVM: Storage packing, immutable/constant usage, library calls.
b. Non-EVM (Rust): Storage tier usage, WASM size, Rust efficiency patterns.

Alright, real talk.

Gas optimization isn't glamorous. It doesn't lend itself to flashy demos or impressive pitch decks. But it's the difference between a protocol users can actually afford to use and one they abandon after the first transaction.

I've seen great projects die because their gas costs were five times higher than those of their competitors. Not because the code was bad, but because it wasn't efficient.

Every blockchain has its quirks, but the fundamentals are universal: plan for efficiency or pay the price.

Whether you're deploying to Ethereum mainnet, launching on Stellar, or building on any other Non-EVM chains, the same patterns apply: minimize storage, batch operations, validate early, and compute off-chain.

Top comments (0)