Skip to content

Gas Limit, Gas Used, and the Fee Formula

The previous page established what gas measures: a single unit that prices every operation the shared computer can perform, from a cheap addition to a state-growing storage write. That gives us a number — a count of work — for any transaction. This page turns that count into money and into a safety guarantee.

To do that we need four quantities and one formula. The formula is almost insultingly simple: you pay for the work you did, at a price per unit of work. The interesting part is everything the formula doesn’t say — what happens when you guessed the budget wrong, why a failed transaction still costs you, and how the same idea, applied to a whole block, is what keeps every node on Earth able to keep up. All of that is transaction-level accounting, and by the end of this page you’ll be able to read a receipt line by line.

Every transaction carries two numbers you choose before you send it, and produces two numbers after it runs.

you set, up front: the chain reports, after execution:
───────────────── ──────────────────────────────────
gas_limit (a budget) gas_used (what execution consumed)
price_per_gas (what you'll pay) fee = gas_used × price_per_gas
  • gas_limit — the ceiling you authorise for this transaction. It is a budget: “do not spend more than this many units of work on my behalf.” It is not a promise to spend it all.
  • price_per_gas — how much native currency (wei, the smallest unit of ETH) you’ll pay for each unit of gas. On modern Ethereum this splits into a base fee and a priority fee — the subject of EIP-1559 — but for the accounting on this page a single per-unit price is all we need.
  • gas_used — the amount of gas execution actually consumed. Known only after the transaction runs, because it depends on which branches the code took and how many storage slots it touched.
  • fee — what leaves your account, given by the formula:
fee = gas_used × price_per_gas

The single most important word in that formula is used. When a transaction succeeds, you pay for gas_used, not for gas_limit. The limit was a ceiling; the fee is metered off the actual meter reading. If you budgeted 100,000 gas and execution consumed 51,000, you pay for 51,000 and the remaining 49,000 is refunded — it never leaves your account.

This is exactly what our companion mini-EVM reports. In ethmini, a successful run returns an Execution whose gas_used is computed as budget minus what’s left on the meter:

// rust/ethmini/src/vm.rs — on a clean STOP:
Op::Stop => {
return Ok(Execution {
gas_used: gas_limit - gas, // what you actually burned, not the limit
return_value: None,
storage: working,
});
}

gas here is the meter, initialised to gas_limit and decremented before every opcode. When the program halts cleanly, whatever it didn’t spend is still sitting in gas, and gas_limit - gas is precisely gas_used. You are charged for the difference, and nothing more.

The comfortable case is a transaction that finishes inside its budget. The whole reason gas exists, though, is the uncomfortable case: a transaction that hits the ceiling before it finishes. As the previous part established, “does this program halt?” is undecidable — so the chain can’t screen out a runaway program by inspection. Instead it runs the program with a countdown, and when the countdown reaches zero it forcibly halts execution. That forced halt is out-of-gas.

Two things happen the instant the meter hits zero, and it is vital to keep them separate:

  1. The transaction reverts. Every state change the execution made — every storage write, every value moved inside the call — is undone, as if the transaction had never touched the world. This is all-or-nothing: you do not get a half-finished contract.
  2. The gas is still consumed and still paid. The work the node did before the halt was real work; the node is compensated for it. An out-of-gas transaction burns its entire budget — you pay for gas_limit, not for the smaller amount that ran before the crash.

That second point surprises people, so it’s worth stating plainly: a reverted transaction is not a free retry. You paid for the computation the network performed on your behalf, even though the result was thrown away. The chain has to charge for it — otherwise an attacker could submit endless expensive transactions that “fail” and cost them nothing, and every node would grind through the work for free.

ethmini models this exactly. When the meter can’t afford the next opcode, it raises OutOfGas and reports used = gas_limit — the whole budget:

// rust/ethmini/src/vm.rs — the meter refuses to go negative:
let charge = |gas: &mut u64, cost: u64| -> Result<(), VmError> {
if *gas < cost {
Err(VmError::OutOfGas {
used: gas_limit, // an exceptional halt burns the ENTIRE budget
limit: gas_limit,
})
} else {
*gas -= cost;
Ok(())
}
};

And the revert is structural, not a cleanup step you have to remember. The VM runs against a clone of the contract’s storage and only hands the new storage back on a clean halt. If it throws, the caller simply keeps the original — the changes were never committed in the first place:

// rust/ethmini/src/state.rs — commit on success, drop on revert:
match vm::run(&code, &storage, *gas_limit) {
Ok(exec) => {
self.account_mut(*to).storage = exec.storage; // commit the new storage
// ... Receipt { status: Success, gas_used: exec.gas_used, .. }
}
Err(reason) => Err(Reverted {
gas_used: *gas_limit, // an exceptional halt burns the whole gas budget
reason,
}),
}

Notice the asymmetry between the two success/failure branches:

OutcomeState changesgas_used chargedNonce consumed?
Success (STOP/RETURN)committedgas_limit - gas (actual)yes
Out-of-gas (revert)undonegas_limit (the whole budget)yes
Rejected (bad nonce, can’t afford value)none — never includednothingno

The third row is a different animal entirely and worth pinning down. A rejection — an unknown sender, a stale nonce, a balance too small to cover the value being sent — means the transaction was never valid to include. It never runs, so it costs no gas and moves no state, not even the nonce. Compare that to a revert, where the transaction was valid, was included, did run, burned gas, and consumed its nonce so it can’t be replayed — it just accomplished nothing. “Rejected” and “reverted” sound alike and are frequently confused; the ethmini design keeps them as two distinct error kinds (EthError vs. VmError) precisely to make the line unmistakable. There’s more on that split on the smart contracts and state pages.

Under the hood — why the whole budget, not just what ran?

Section titled “Under the hood — why the whole budget, not just what ran?”

You might expect out-of-gas to charge only for the opcodes that executed before the halt — say, 9,998 gas out of a 10,000 budget. Real EVM (and ethmini) charges the full limit instead. The reason is that the alternative creates a perverse incentive.

If a failed transaction only cost what it consumed, an attacker could set a huge limit, do enormous work, and arrange to run out one opcode from the end — paying only for the fraction that “counted” and stiffing the network for the rest. By charging the entire authorised budget on an exceptional halt, the protocol makes the cost of a failure equal to the cost you committed to. You told the network “you may spend up to this much on me”; if your program blows the budget, you honour that commitment in full. The meter is a promise, and out-of-gas is the protocol collecting on it.

Two budgets that share a word: the block gas limit

Section titled “Two budgets that share a word: the block gas limit”

So far “gas limit” has meant your transaction’s ceiling. There is a second, completely different ceiling with almost the same name, and conflating the two is one of the most common beginner mistakes.

  • The per-transaction gas limit is a budget you set on your transaction. It bounds how much work one transaction may do.
  • The block gas limit is a protocol-wide ceiling on the sum of gas_used across every transaction packed into a single block. It bounds how much work the whole block may demand of the network.
┌─────────────────────── block gas limit (e.g. ~30–60M gas) ──────────────────────┐
│ │
│ ┌── tx A ──┐ ┌──── tx B ────┐ ┌─ tx C ─┐ ┌──── tx D ────┐ ┌ ... ┐ │
│ │ 21,000 │ │ 180,000 │ │ 55,000 │ │ 240,000 │ │ │ (unused) │
│ └──────────┘ └──────────────┘ └────────┘ └──────────────┘ └─────┘ │
│ each tx's own gas_limit caps its slot; Σ gas_used ≤ block gas limit │
└──────────────────────────────────────────────────────────────────────────────────┘

The two limits enforce different guarantees. Your per-transaction limit protects you (from a buggy contract draining your wallet) and protects the network (from your transaction running forever). The block gas limit protects everyone: it guarantees that no matter what transactions a block contains, processing the entire block takes a bounded amount of work. Without it, a single block could demand hours of computation from every node, and nodes that couldn’t keep up would fall off the chain — a network that only the most powerful machines could follow is a centralised network. The block limit is what keeps the shared computer runnable on ordinary hardware, which is what keeps it decentralised.

A transaction whose own gas limit exceeds the remaining space in a block simply can’t fit and is left for a later block. And no valid block can ever have its transactions’ total gas_used exceed the block gas limit — that is a consensus rule every node checks, so a block that broke it would be rejected by the whole network.

Why a too-low limit is dangerous and a too-high one is safe

Section titled “Why a too-low limit is dangerous and a too-high one is safe”

Here is the practical asymmetry that falls straight out of the rules above, and it’s the reason wallets do gas estimation for you.

  • Set the limit too low and your transaction runs, does real work, hits the ceiling partway through, and reverts — but you still pay. You get the worst outcome: a fee charged (the whole budget) and nothing accomplished. Your storage write, your token transfer, your swap — all undone, all paid for.
  • Set the limit too high and nothing bad happens. Execution uses whatever it actually needs, you’re charged for that gas_used, and every unit of the surplus is refunded. The extra headroom cost you exactly zero.
limit too LOW: [████████████░] boom → OutOfGas → REVERT, pay full limit, gain nothing
limit too HIGH: [██████░░░░░░░] STOP → success, pay gas_used, refund the rest ✓
^^^^^^ used ^^^^^^ refunded

The asymmetry is stark: undershooting the limit is a paid failure, overshooting it is free insurance. So the sane strategy is to estimate the real cost and add comfortable headroom on top. But you can’t set the limit absurdly high and forget it either — your limit still has to fit within the block’s remaining space, and if your price-per-gas is high, a wallet may warn you that a huge limit could, in a worst case, authorise a large fee. The real skill is estimating gas_used accurately, which is genuinely hard because it depends on execution paths and current state. That’s a page of its own: fee estimation and blockspace economics.

The per-transaction gas limit is a small idea with outsized consequences — a user-set budget on untrusted computation. It’s worth viewing through the five questions.

  • Why does it exist? Because the chain runs your code but can’t know in advance how much work it will take, and can’t let it run unbounded. The limit is you pre-committing a ceiling so the network can safely run your program with a countdown that’s guaranteed to terminate.
  • What problem does it solve? It converts the undecidable “will this halt?” into the decidable “will this halt within N gas?”, while also protecting your wallet from a buggy or malicious contract spending more than you authorised.
  • What are the trade-offs? You must guess the right budget before you know the answer. Guess too low and you pay for a revert that accomplished nothing; guess too high and you (usually) lose nothing but the mental overhead — motivating a whole layer of estimation tooling.
  • When should I avoid it? You can’t; it’s mandatory on every transaction. But you should avoid setting it by hand to a tight value — let a wallet estimate and add headroom, because the failure mode of undershooting is a paid revert.
  • What breaks if I remove it? Without a per-transaction limit, a single transaction could loop forever or grow state without bound, and there’d be no cap on the fee a buggy contract could drain from your account. Combined with removing the block limit, blocks could demand unbounded work and only the fastest machines could follow the chain — decentralisation collapses.
  1. Distinguish gas_limit from gas_used. Which one do you set, which one is reported, and which one is your fee actually computed from when a transaction succeeds?
  2. Write the fee formula, and use it to compute the fee (in ETH) of a plain transfer that used 21,000 gas at 20 gwei per gas.
  3. A transaction runs out of gas. What happens to (a) its state changes, (b) the gas, and (c) how much of the budget is charged? Relate each to a specific behaviour in ethmini.
  4. Explain the difference between the block gas limit and the per-transaction gas limit — same word, two budgets. What does each one guarantee?
  5. Why is setting your gas limit too low dangerous while setting it too high is safe? What does this asymmetry imply about how you should choose a limit?
Show answers
  1. You set gas_limit up front — a ceiling / budget for the transaction. The chain reports gas_used after execution — the amount actually consumed. On success your fee is computed from gas_used (fee = gas_used × price_per_gas), and any unused gas is refunded; you do not pay for the full limit. In ethmini this is gas_limit - gas on a clean STOP/RETURN.
  2. fee = gas_used × price_per_gas. With 21,000 gas at 20 gwei: 21,000 × 20 gwei = 420,000 gwei = 420,000 × 10⁻⁹ ETH = 0.00042 ETH.
  3. (a) All state changes are undone — the transaction reverts, all-or-nothing. (b) The gas is still consumed and paid (the work was real). (c) The entire budget (gas_limit) is charged, not just what ran. In ethmini, the charge closure raises OutOfGas { used: gas_limit, .. }, the VM ran against a clone of storage so the writes were never committed, and state.rs returns Reverted { gas_used: gas_limit }.
  4. The per-transaction limit is a ceiling you set on your transaction, bounding how much work it may do (protecting your wallet and forcing your program to terminate). The block gas limit is a protocol-wide ceiling on the sum of gas_used across all transactions in a block, guaranteeing every node can process any block in bounded time — which is what keeps the chain runnable on ordinary hardware, and therefore decentralised.
  5. Too low: execution hits the ceiling, reverts, undoes everything, but you still pay the full budget — a paid failure that accomplished nothing. Too high: you’re charged only for actual gas_used and the surplus is refunded, so the headroom is effectively free. The asymmetry means you should estimate the real cost and add comfortable headroom, never set the limit tight — which is why wallets do gas estimation for you.