After the London fork, Ethereum transaction fees split into a deterministic base fee and an optional priority fee. The eth_feeHistory RPC method returns per-block base fees and, when requested, priority fee percentiles from included transactions. This article explains how to read that data and combine it into a robust gas price estimate that avoids overpaying or relying on a single eth_gasPrice oracle.
The EIP-1559 Fee Market in Practice
Since the London hard fork, Ethereum transaction pricing is no longer a single gasPrice auction. Each block has a baseFeePerGas that is algorithmically determined based on the gas used in the previous block. This base fee is burned, not paid to miners, and can adjust up or down by a maximum of 12.5% per block as it tries to keep block gas usage near a target. Users then add an optional priority fee (often called a tip) to incentivize validators to include their transaction. The total fee is capped by the maxFeePerGas you set, and the priority portion is capped by maxPriorityFeePerGas.
The official Ethereum execution-API specification defines the eth_feeHistory method, which returns a history of base fees and gas usage ratios for a range of blocks. When you request rewardPercentiles, the client scans the transactions included in those blocks and returns the priority fees paid at the requested percentiles. This is the raw material for a data-driven gas estimator.
For a quick overview of the Ethereum network and its RPC endpoints, see the Ethereum network page.
Reading eth_feeHistory Response
The eth_feeHistory method takes three parameters: blockCount (the number of blocks to fetch), newestBlock (the highest block number or the tag latest), and rewardPercentiles (an array of percentiles, e.g., [25, 50, 75, 90]). The response contains three key fields: baseFeePerGas (an array of base fees for each block, plus one extra for the next block), gasUsedRatio (the ratio of gas used to gas limit for each block), and reward (an array of arrays, each containing the priority fee at the requested percentiles for that block).
The reward field is only populated when you request rewardPercentiles. If you omit it, the client returns an empty array. Also, the way clients compute these percentiles can vary: some scan all transactions, others sample, and public RPC providers may limit the depth of history or even omit the reward data to reduce load. This is documented behavior that varies by client and provider; always test with your specific endpoint.
The baseFeePerGas array has one more element than the number of blocks requested. The last element is the base fee of the block that would follow the newest block, assuming no change in gas usage. This is useful for projecting the next block's base fee.
Building a Simple Estimator with Node.js
The following script calls eth_feeHistory for the last 10 blocks, prints the base fees and gas usage ratios, and then computes a suggested maxFeePerGas and maxPriorityFeePerGas pair. The estimation method is deliberately simple: it takes the 90th percentile priority fee from the history and adds a 12.5% headroom to the average base fee to account for possible increases.
You can run this script with any Ethereum RPC endpoint, including a public one or your own node. Replace YOUR_RPC_URL with your endpoint. The script uses the fetch API available in Node.js 18+.
const RPC_URL = 'YOUR_RPC_URL';
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 data = await res.json();
if (data.error) throw new Error(data.error.message);
return data.result;
}
async function estimateGas() {
const blockCount = 10;
const newestBlock = 'latest';
const rewardPercentiles = [25, 50, 75, 90];
const history = await rpc('eth_feeHistory', [blockCount, newestBlock, rewardPercentiles]);
console.log('Base fees per block:');
console.log(history.baseFeePerGas);
console.log('Gas used ratios:');
console.log(history.gasUsedRatio);
console.log('Reward percentiles per block:');
console.log(history.reward);
// Compute average base fee from the first blockCount entries (exclude the extra next-block estimate)
const baseFees = history.baseFeePerGas.slice(0, blockCount).map(hex => parseInt(hex, 16));
const avgBaseFee = baseFees.reduce((a, b) => a + b, 0) / baseFees.length;
// Take the 90th percentile priority fee from the last block that has data
const lastRewards = history.reward[history.reward.length - 1];
const p90Priority = lastRewards ? parseInt(lastRewards[3], 16) : 0; // index 3 corresponds to 90th percentile
// Add 12.5% headroom to base fee for possible increase
const projectedBaseFee = Math.ceil(avgBaseFee * 1.125);
const maxPriorityFeePerGas = p90Priority;
const maxFeePerGas = projectedBaseFee + maxPriorityFeePerGas;
console.log(`\nSuggested maxPriorityFeePerGas: ${maxPriorityFeePerGas}`);
console.log(`Suggested maxFeePerGas: ${maxFeePerGas}`);
}
estimateGas().catch(console.error);Expected Output and How to Interpret It
When you run the script, you'll see output similar to the following (values are examples, not measurements):
The base fees are in wei (hexadecimal). The gas used ratio tells you how full each block was: a ratio above 0.5 means the base fee will likely increase in the next block, while below 0.5 means it will decrease. The reward percentiles show the priority fees paid by transactions at each percentile. For example, the 90th percentile value means 90% of transactions paid that tip or less.
To validate the estimator, you can record your own results in a table like this:
| Block range | Avg base fee (Gwei) | 90th pct tip (Gwei) | Suggested maxFee (Gwei) | Actual next block base fee (Gwei) |
|---|---|---|---|---|
| 10 blocks | 20 | 1.5 | 23.5 | 20.1 |
| 20 blocks | 19 | 2.0 | 23.4 | 19.8 |
Base fees per block:
["0x3b9aca00", "0x3b9aca00", ...]
Gas used ratios:
[0.5, 0.6, ...]
Reward percentiles per block:
[["0x3b9aca00", "0x4a817c80", ...], ...]
Suggested maxPriorityFeePerGas: 1000000000
Suggested maxFeePerGas: 2000000000Choosing Percentiles and Horizon
The choice of rewardPercentiles and the number of blocks to fetch depends on your tolerance for confirmation time versus cost. A common approach is to use the 25th, 50th, 75th, and 90th percentiles. If you need fast inclusion, use the 90th percentile tip; if you are willing to wait, the 50th percentile might suffice.
The horizon (blockCount) should reflect recent network conditions. A short horizon (e.g., 5-10 blocks) reacts quickly to changes but may be noisy. A longer horizon (e.g., 50-100 blocks) smooths out short-term spikes but may lag during congestion. For most applications, 10-20 blocks is a reasonable balance.
Remember that the base fee can change by up to 12.5% per block. If you are sending a transaction that might not be mined immediately, you should project the base fee over the expected number of blocks until inclusion. A simple method is to take the average base fee over the last N blocks and add a safety margin, as shown in the script.
Avoiding Double-Counting the Base Fee
A common mistake is to add the priority fee percentile to the current base fee and set that as maxFeePerGas. However, the priority fees reported by eth_feeHistory are the tips that were paid on top of the base fee at that time. If the base fee has since increased, the actual total fee paid by those historical transactions was higher, but the tip alone is what you need to add to the projected base fee.
In other words, your maxFeePerGas should be the projected base fee (with headroom) plus your chosen priority fee. The priority fee is the tip you are willing to pay, independent of the base fee. This is the correct way to budget for EIP-1559 transactions.
For a deeper dive into transaction simulation and state overrides, see our guide on eth_call state overrides and simulation.
When to Use eth_maxPriorityFeePerGas
Many clients provide a convenience method eth_maxPriorityFeePerGas that returns a suggested tip based on recent network conditions. This is a quick shortcut, but it does not include the base fee. You still need to set maxFeePerGas yourself, typically by adding the suggested tip to a projected base fee.
Using eth_maxPriorityFeePerGas can be a reasonable fallback if you don't want to implement percentile sampling yourself, but it is less transparent and may not reflect your specific inclusion requirements. For a production wallet, you might combine both: use eth_feeHistory for base fee projection and eth_maxPriorityFeePerGas as a sanity check.
Limitations and Tradeoffs
Base fee is not a price oracle for inclusion. A low base fee does not guarantee your transaction will be included if the network is congested; you still need a competitive priority fee. Also, the 12.5% per-block change limit applies to consecutive blocks, but over many blocks the base fee can move significantly, so your projection should account for that.
Different transaction types require different handling. Type-0 legacy transactions use a single gasPrice that covers both base fee and tip. Type-2 transactions (EIP-1559) use maxFeePerGas and maxPriorityFeePerGas. If you are sending a legacy transaction, you cannot use eth_feeHistory directly; you need to estimate a gasPrice that is high enough to cover the base fee plus a tip.
Reorgs can shift the history you fetched. If a block is reorged, the base fee and rewards in that block may change. For critical applications, you might want to wait for a few confirmations before relying on the data.
Public RPC providers may have different limits on blockCount or may not support rewardPercentiles on all networks. Some networks, like certain Layer 2s, may not implement EIP-1559 or may have different fee mechanisms. Always check the documentation for your specific network.
Troubleshooting Common eth_feeHistory Issues
If you encounter issues, here are common problems and fixes:
- Reward fields are empty: This happens when you don't request
rewardPercentilesor when the client/provider does not support percentile sampling. Try addingrewardPercentilesto your request, or use a different RPC provider.
- HTTP 415 Unsupported Media Type: Some networks or providers may not support
eth_feeHistoryat all. Check the network's documentation or use an alternative method likeeth_gasPriceas a fallback.
- blockCount limits: Some providers cap the number of blocks you can request in one call. If you get an error, reduce
blockCountand make multiple calls if needed.
- Hex vs decimal: Remember that all numeric values in the response are hex-encoded. Use
parseIntwith base 16 to convert.
For more on RPC reliability, see our guides on Ethereum RPC timeouts and retries and Ethereum RPC latency and performance.
Next Steps and Further Reading
Now that you understand how to read eth_feeHistory, you can build a more sophisticated estimator that adapts to network conditions. Consider integrating this into your wallet or relay to reduce overpayment. For a broader understanding of Ethereum RPC, explore the OnFinality Learn hub and the API service documentation.
If you are choosing an Ethereum node provider, our Choosing an Ethereum RPC node guide can help you evaluate features like eth_feeHistory support. Also, review RPC pricing to understand cost implications of high-frequency calls.
For related topics, see our guides on EVM nonce management with eth_getTransactionCount and eth_call state overrides and simulation.