Sending EIP-1559 Transactions with web3.js: Predictable Gas Fees Without the Guesswork
Legacy gasPrice transactions make you guess in a first-price auction. Here's how to use web3.js's EIP-1559 fields — maxFeePerGas and maxPriorityFeePerGas — to cap costs, get automatic refunds, and verify what you actually paid.
21 Nov 2025, 17:54 UTC

If your app still sends transactions with a single gasPrice field, you're paying for a fee model that Ethereum replaced in August 2021 with the London hard fork. Legacy pricing forces you to guess one number that competes in a first-price auction — guess low and your transaction stalls, guess high and you overpay. EIP-1559 splits fees into a protocol-burned base fee and an optional priority tip, and web3.js (v1.3 and later) supports the typed transaction format directly. The practical payoff: you can cap what you're willing to pay while letting the network refund the difference automatically.
How the two fee fields actually work
An EIP-1559 (type 0x2) transaction replaces gasPrice with two values:
maxPriorityFeePerGas— the tip paid to the block proposer. This is your bidding lever for inclusion speed.maxFeePerGas— the absolute ceiling per unit of gas, covering base fee plus tip. You are chargedbaseFee + priorityFee(capped at your max), and any unused headroom is refunded.
When web3.js sees these fields in a transaction object, it serializes the transaction as type 0x2 and omits gasPrice. Setting both gasPrice and the EIP-1559 fields is contradictory — pick one model.
Check your environment before sending
Two things must be true, and both are cheap to verify:
- Library version. The EIP-1559 fields were added in web3.js v1.3.0. Run
node -e "console.log(require('web3').version)"in your project directory and confirm you're on v1.3.0 or newer (this also applies to the 4.x line, where the same fields are supported). - Chain support. The connected node must be on a post-London network. From any script with a configured provider, call
await web3.eth.getBlock('latest')and check that the returned block has abaseFeePerGasproperty. If it's absent, the chain predates London (or is a legacy-fee sidechain) and you must fall back togasPrice.
Sending a type-2 transaction to a pre-London node gets it rejected outright, so this check belongs in your startup or health-check path, not your error handler.
Worked example: estimate, sign, send
The snippet below runs in a Node.js script with a local (unlocked-by-you) private key and an HTTP provider such as a node you operate or an RPC endpoint. It uses feeHistory to look at recent blocks, picks a tip, and adds headroom above the current base fee so the transaction survives a couple of blocks of base-fee increases.
const { Web3 } = require('web3');
const web3 = new Web3('https://your-rpc-endpoint');
async function send1559() {
const account = web3.eth.accounts.privateKeyToAccount('0xYOUR_PRIVATE_KEY');
web3.eth.accounts.wallet.add(account);
// Look at the last 5 blocks; reward percentile 50 = median tip
const history = await web3.eth.feeHistory(5, 'latest', [50]);
const baseFee = BigInt(history.baseFeePerGas.at(-1));
const medianTip = BigInt(
history.reward.map(r => BigInt(r[0])).reduce((a, b) => a + b) /
BigInt(history.reward.length)
);
const maxPriorityFeePerGas = medianTip;
// 2x base fee headroom + tip; unused portion is refunded
const maxFeePerGas = baseFee * 2n + maxPriorityFeePerGas;
const receipt = await web3.eth.sendTransaction({
from: account.address,
to: '0xRECIPIENT_ADDRESS',
value: web3.utils.toWei('0.01', 'ether'),
maxFeePerGas: maxFeePerGas.toString(),
maxPriorityFeePerGas: maxPriorityFeePerGas.toString()
});
console.log('Mined in block', receipt.blockNumber,
'effective gas price', receipt.effectiveGasPrice);
}
send1559().catch(console.error);Run this with a funded testnet account first (e.g., Sepolia). The effectiveGasPrice field on the receipt is your verification: it should be at or below your maxFeePerGas, and the difference between your cap and the effective price is the refund you kept. The same fields work on contract calls — pass them in the options object of contract.methods.myMethod(...).send({ from, maxFeePerGas, maxPriorityFeePerGas }).
Note that web3.eth.sendTransaction with a wallet-added account signs locally and submits the raw transaction, so your key never leaves the process. If you prefer explicit control, web3.eth.accounts.signTransaction plus web3.eth.sendSignedTransaction accepts the same fields.
Trade-offs and where this goes wrong
The main limitation is estimation quality. feeHistory is backward-looking; a sudden congestion spike (a popular NFT mint, a liquidation cascade) can push the base fee past your maxFeePerGas within a block or two, and your transaction then sits pending until fees fall or you replace it with a higher cap (same nonce, higher fees). The 2x base-fee multiplier above is a common heuristic, not a guarantee — time-critical transactions may want more headroom, and patient ones can set maxFeePerGas closer to the current base fee to avoid overpaying during a spike.
Second, not every network behaves like mainnet. Some L2s and sidechains have their own fee semantics, near-zero base fees, or partial EIP-1559 support, so hardcoding assumptions breaks portability. Always derive fees from the connected chain at send time rather than caching constants.
Finally, a pending transaction with a low tip competes poorly when block space is scarce — the base fee gets you considered, but the tip gets you picked. Setting maxPriorityFeePerGas to near zero is a valid strategy only if you genuinely don't care when (or whether, under sustained congestion) the transaction lands.
Putting it into practice
The actionable path: verify your web3.js version and the chain's baseFeePerGas support once at startup, estimate fees with feeHistory at send time, cap with maxFeePerGas = 2 × baseFee + tip as a starting heuristic, and confirm via effectiveGasPrice on the receipt that you paid what you expected. Add a pending-transaction monitor that re-prices with the same nonce if inclusion takes too long, and you'll have fee behavior that's both cheaper and far more predictable than the legacy auction ever was.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.