eth_estimateGas runs a simulated execution against a specific chain state and returns the gas that simulation consumed, not a guaranteed limit for on-chain inclusion. Estimates go wrong for four main reasons: state drift between simulation and inclusion, calls that succeed in simulation but revert in a changed block, provider differences in honouring the optional block and state-override parameters, and early termination on revert that reports gas used up to the failure point. A safe limit is derived by multiplying the estimate by a configurable safety factor, capping it against the block gas limit, and checking affordability against the sender balance. eth_call returns return data and reverts on failure, while eth_estimateGas returns a quantity and errors when the call reverts, so the two are complementary verification tools. The intrinsic-gas floor of 21000 plus calldata and contract-creation cost must be included in any limit.
What eth_estimateGas Actually Returns
The Ethereum JSON-RPC specification defines eth_estimateGas as a method that generates and returns an estimate of how much gas is necessary to allow a transaction to complete. The key word is estimate: the node executes the call against a specific state and returns the gas that execution consumed, which is a measurement of one simulation, not a promise about a future block. The specification also documents an optional block parameter, so the same call can be evaluated at latest, at a pinned historical block, or at pending.
When you pass a full transaction object, the returned quantity includes the intrinsic cost of the transaction, which the Ethereum Yellow Paper fixes at 21000 gas for a plain transfer plus calldata and contract-creation costs. When you pass only a call object without a from or nonce, some clients treat it as a call and may not add the full intrinsic cost. This ambiguity is one of the most common sources of a limit that is too low, and it is why the parameter object you send matters as much as the number you get back.
Because the result is a simulation, it reflects the state at the block you asked for. If that state changes before your transaction is included, the estimate can be wrong even though the node answered correctly. Treat the number as an input to a limit calculation, not as the limit itself.
The method contract used throughout is defined by the Ethereum JSON-RPC specification for eth_estimateGas and eth_call, the split between the gas limit and the fee parameters comes from EIP-1559, and the intrinsic gas floor (21000 plus calldata cost) is defined in the Ethereum Yellow Paper. Treat those as the source of truth and this article as the operational procedure built on top of them.
- Returns a gas quantity consumed by a simulated execution at a given state.
- Includes 21000 intrinsic gas when a full transaction object is supplied.
- Honours an optional block parameter, so latest and a pinned block can differ.
- Is not a guarantee of on-chain success; it is a measurement of one simulation.
The Four Reasons a Gas Estimate Is Wrong
State drift is the first cause. The estimate is taken at block N, but your transaction may be included at block N+3 after other transactions have changed balances, allowances, or contract storage. A swap that estimated 150000 gas at block N can need more at block N+3 if a pool ratio moved and a different branch executed. This is documented protocol behaviour: the estimate is only as good as the state it was taken at.
The second cause is a call that succeeds at simulation but reverts in a block whose state already changed. A transaction that depends on another pending transaction, such as an approval that has not yet been mined, estimates against a state where the approval does not exist. The simulation may still succeed if the contract tolerates the missing allowance, but the real transaction can revert when the dependency lands differently.
The third cause is provider differences. The Ethereum JSON-RPC specification documents the optional block parameter and state-override sets, but whether a given provider honours them, and how it treats a missing from field, varies by provider. Some endpoints ignore a pinned block and always estimate at latest; others reject state overrides. Always confirm the behaviour of your endpoint rather than assuming the specification is fully implemented.
The fourth cause is early termination on revert. When the simulated call reverts, the node reports the gas consumed up to the revert point, not the gas the successful path would need. If you take that number as your limit, you are budgeting for a failure, not for success. This is why a revert reason is often only visible through eth_call with the same parameters.
- State drift between estimate and inclusion changes which branch executes.
- A call that depends on a pending transaction estimates against a state that will not exist.
- Provider handling of the block parameter and state overrides varies by provider.
- A revert stops the simulation early and reports gas used up to the failure point.
eth_call Versus eth_estimateGas: Choosing the Right Probe
The Ethereum JSON-RPC specification describes eth_call as a method that executes a new message call immediately without creating a transaction, returning the return data of the call. If the call reverts, eth_call returns an error, and many clients include the revert reason in that error. eth_estimateGas returns a quantity, and it also returns an error when the call reverts, but the error is about the estimate rather than the return value.
Use eth_call when you need the return data, when you want to surface a revert reason, or when you are checking whether a path succeeds at a given state. Use eth_estimateGas when you need a gas quantity to build a transaction. In practice you use both: estimate to get a starting number, then call with the same parameters to confirm the path succeeds and to capture any revert reason. The article on decoding Ethereum revert reasons and custom errors covers how to turn those errors into readable messages.
A useful pattern is to run eth_call first with the exact parameters you intend to send. If it reverts, fix the call before you spend time on gas estimation. If it succeeds, run eth_estimateGas at the same block and compare. A large gap between the two is a signal that the estimate is being taken at a different state or with different defaults.
- eth_call returns return data and errors on revert, exposing the revert reason.
- eth_estimateGas returns a gas quantity and errors when the call reverts.
- Run eth_call first to validate the path, then eth_estimateGas to size the limit.
- Compare both at the same block to detect state or default mismatches.
The Intrinsic-Gas Floor and the Contract-Creation Surcharge
The Ethereum Yellow Paper defines intrinsic gas as the base cost a transaction pays before any contract code runs. For a plain value transfer this is 21000 gas. Calldata adds cost per byte, with a lower cost for zero bytes and a higher cost for non-zero bytes, and contract creation adds a surcharge on top of the base. A safe limit must include all of these, because a limit below the intrinsic floor fails before execution begins.
Whether eth_estimateGas includes the intrinsic cost depends on the parameters you pass. When you supply a full transaction object with a from address and a to address, the node can compute the intrinsic cost and include it. When you supply a bare call object, some clients estimate only the execution cost. This is documented behaviour in the specification's parameter description, but the practical effect varies by provider, so verify with a known simple transfer.
A quick sanity check is to estimate a plain transfer to an externally owned account. If the result is near 21000, the endpoint is including intrinsic gas. If it is far lower, you are looking at execution cost only and must add the floor yourself. This single check prevents a large class of too-low limits.
- 21000 gas is the base cost for a plain transfer.
- Calldata adds per-byte cost, with zero and non-zero bytes priced differently.
- Contract creation adds a surcharge above the base and calldata cost.
- Verify whether your endpoint includes intrinsic gas by estimating a plain transfer.
Deriving a Safe Limit from the Estimate
The first step is to multiply the estimate by a configurable safety factor. A factor of 1.2 to 1.5 is a common starting range for simple transfers and stable contract calls, while more state-dependent calls may need more. The factor is a policy choice, not a protocol constant, and it should be tuned against your own measurements rather than copied from a blog post.
The second step is to cap the result against the block gas limit. A limit above the block gas limit can never be included, and a limit close to it is a sign that the estimate is wrong or that the call is pathologically expensive. Read the block gas limit from the latest block and clamp your derived limit below it.
The third step is to check affordability against the sender balance. Under EIP-1559, the gas limit is the budget and the fee parameters are the price, and the node checks that the sender can cover limit multiplied by maxFeePerGas plus the value. A generous limit ties up balance even though only the used gas is charged, so a limit that is safe for execution can still make a transaction unaffordable. The EIP-1559 fee-market article covers the price side, which this article deliberately keeps separate from the limit.
- Multiply the estimate by a configurable safety factor, typically 1.2 to 1.5.
- Cap the derived limit below the current block gas limit.
- Check that balance covers limit times maxFeePerGas plus value.
- Keep the limit decision separate from the fee-price decision.
A Runnable Node.js Estimator with Buffer and Verification
The script below calls eth_estimateGas with a full transaction object at a pinned block, applies a buffer, caps the result against the block gas limit, and then verifies the same call with eth_call. It uses only the built-in fetch available in modern Node.js, so there is no dependency to install. Replace the endpoint with your own from the Ethereum RPC URL and endpoint selection guide.
The script reads the block gas limit from eth_getBlockByNumber at the same pinned block, so the cap reflects the state you estimated against. It also prints the raw estimate, the buffered limit, and the capped limit so you can see each stage. If eth_call returns an error, the script prints the revert reason, which is the fastest way to distinguish a bad call from a bad estimate.
const RPC_URL = process.env.RPC_URL || "https://your-endpoint.example";
const PINNED_BLOCK = process.env.BLOCK || "latest";
const SAFETY_FACTOR = Number(process.env.SAFETY_FACTOR || 1.3);
async function rpc(method, params) {
const res = await fetch(RPC_URL, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params })
});
const json = await res.json();
if (json.error) throw new Error(method + " -> " + JSON.stringify(json.error));
return json.result;
}
async function main() {
const tx = {
from: "0xYourSenderAddress",
to: "0xRecipientOrContract",
value: "0x0",
data: "0x",
maxFeePerGas: "0x3b9aca00",
maxPriorityFeePerGas: "0x3b9aca00"
};
const block = await rpc("eth_getBlockByNumber", [PINNED_BLOCK, false]);
const blockGasLimit = BigInt(block.gasLimit);
const estimateHex = await rpc("eth_estimateGas", [tx, PINNED_BLOCK]);
const estimate = BigInt(estimateHex);
const buffered = (estimate * BigInt(Math.round(SAFETY_FACTOR * 100))) / 100n;
const capped = buffered > blockGasLimit ? blockGasLimit : buffered;
console.log("raw estimate:", estimate.toString());
console.log("buffered limit:", buffered.toString());
console.log("capped limit:", capped.toString());
try {
const returnData = await rpc("eth_call", [tx, PINNED_BLOCK]);
console.log("eth_call ok, return data:", returnData);
} catch (err) {
console.log("eth_call reverted:", err.message);
}
}
main().catch((e) => { console.error(e.message); process.exit(1); });A Results Table to Fill Against Your Own Endpoint
Provider behaviour varies, so the only reliable way to know how your endpoint handles eth_estimateGas is to measure it. Run the script above against several call types at the same pinned block, then repeat at latest, and record the numbers. The table below is a template; fill it with your own measurements rather than relying on any published figure.
The most informative rows are the plain transfer and the state-dependent call. A plain transfer near 21000 confirms intrinsic gas is included. A state-dependent call that differs between a pinned block and latest confirms that the block parameter is honoured. If the two rows are identical for a call you know is state-dependent, your endpoint may be ignoring the block parameter.
- Columns: call type, block parameter, raw estimate, buffered limit, capped limit, eth_call result.
- Rows: plain transfer, ERC-20 transfer, state-dependent swap, contract creation.
- Repeat each row at latest and at a pinned block to detect block-parameter handling.
- Record the block gas limit alongside the capped limit for context.
Troubleshooting Common eth_estimateGas Failures
A -32000 execution reverted error from eth_estimateGas means the simulated call failed. The estimate method returns an error rather than a number in this case, and the revert reason is often only visible through eth_call with the same parameters. Run eth_call first, capture the reason, and fix the call before retrying the estimate. The JSON-RPC -32603 internal error debugging guide covers the adjacent class of node-side errors that are not reverts.
A gas required exceeds allowance error usually means the sender balance cannot cover the limit multiplied by the fee parameters plus the value. This is an affordability failure, not an execution failure. Reduce the limit if it is inflated, or fund the account. Remember that under EIP-1559 the full limit is checked up front even though only the used gas is charged.
An estimate that returns the block gas limit for an infinite-loop path is a signal that the simulation hit the cap. Do not send that limit. Instead, inspect the contract logic, confirm the loop terminates, and if the path is genuinely unbounded, treat it as a design problem rather than a gas problem. A revert reason that is only visible through eth_call is the fourth common case: the estimate reports a failure without the reason, while eth_call surfaces it.
- -32000 execution reverted: run eth_call with the same parameters to get the reason.
- gas required exceeds allowance: check balance against limit times maxFeePerGas plus value.
- Estimate equal to the block gas limit: suspect an unbounded loop, do not send it.
- Revert reason missing from the estimate: eth_call is the probe that exposes it.
Limitations and Tradeoffs of Buffered Limits
A generous limit ties up balance. Under EIP-1559 the node checks that the sender can cover the limit multiplied by maxFeePerGas plus the value, so a limit that is twice what is needed reserves twice the balance for the duration of the transaction. On accounts that batch many transactions, this can force serialization or require a higher balance than the actual spend.
The estimate is only as good as the state it was taken at. A pinned block gives reproducibility but goes stale as the chain advances; latest gives freshness but is not reproducible. Neither solves the dependency problem where your transaction's outcome depends on another pending transaction. For those cases, consider ordering your transactions explicitly and re-estimating after each dependency lands.
Finally, the safety factor is a policy, not a protocol guarantee. A factor that is safe for one contract may be too small for another, and a factor that is safe today may be too small after a contract upgrade. Treat the factor as a tunable parameter that you revisit with your own measurements, and keep the limit decision separate from the fee-price decision so you can reason about each independently.
- A high limit reserves balance up front even though only used gas is charged.
- Pinned-block estimates are reproducible but stale; latest is fresh but not reproducible.
- Dependencies on pending transactions are not solved by any block parameter.
- The safety factor is a tunable policy that needs periodic re-measurement.
Next Steps for Production Gas Handling
Start by instrumenting your estimator to log the raw estimate, the buffered limit, the capped limit, and the actual gas used after inclusion. Over a few hundred transactions you will see whether your safety factor is too tight or too loose, and you can tune it with evidence rather than guesswork. The eth_feeHistory reward percentiles article covers the price side of the same pipeline.
Pair the limit logic with nonce management so that dependent transactions are ordered correctly. The EVM nonce management guide explains how to read and sequence nonces, which is what makes a re-estimate after each dependency land meaningful. For endpoint selection and failover, review the Ethereum network page and the API service overview, and check RPC pricing if you plan to run high-volume estimation. The OnFinality Learn hub collects the related fee, nonce, and error-handling articles in one place.
- Log raw estimate, buffered limit, capped limit, and actual gas used.
- Tune the safety factor from your own inclusion data.
- Sequence dependent transactions with correct nonce management.
- Choose endpoints that honour the block parameter and state overrides you rely on.