Choosing Between Polling and Subscriptions for Ethereum Transaction Confirmation with web3.js
Learn when to poll for Ethereum transaction receipts and when to use web3.js subscriptions, with a worked ERC‑20 transfer example and practical trade‑offs.
22 Sept 2025, 15:55 UTC

Problem: Waiting for a transaction to be mined without wasting node resources
When you send a transaction with web3.js you need to know whether it succeeded or reverted. The naive approach is to call web3.eth.getTransactionReceipt in a tight loop, but that burns RPC capacity and adds latency. A more efficient alternative is to listen for new heads or logs via a subscription, which requires a persistent connection and reconnection handling.
Thesis: Use polling for simple scripts or environments without WebSocket support; switch to a subscription‑based listener when you have a reliable WS/IPC link and want near‑real‑time confirmation with lower node overhead.
How polling works
After sending a transaction you repeatedly call getTransactionReceipt until it returns a non‑null object or a timeout expires. The receipt contains status (0 = reverted, 1 = success per EIP‑658), blockNumber, gasUsed, and logs. While the transaction is pending the call returns null.
// Run in a Node.js script with web3.js v1.x installed
async function waitForReceiptPolling(txHash, options = {}) {
const { interval = 2000, timeout = 30000 } = options;
const start = Date.now();
while (Date.now() - start < timeout) {
const receipt = await web3.eth.getTransactionReceipt(txHash);
if (receipt) {
return receipt;
}
await new Promise(res => setTimeout(res, interval));
}
throw new Error('Transaction receipt not found within timeout');
}
// Example usage
const tx = await myToken.methods.transfer(recipient, amount)
.send({ from: senderAddress });
const receipt = await waitForReceiptPolling(tx.transactionHash);
console.log('Status:', receipt.status); // 1 = success, 0 = reverted
console.log('Gas used:', receipt.gasUsed);
Permissions: The account used in .send({from}) must have sufficient ether for gas and token balance for the transfer. Risks: Setting interval too low can flood the node; a too‑high interval increases perceived latency.
How a subscription‑based approach works
With a WebSocket or IPC provider you can subscribe to newHeads (or a log filter for the Transfer event) and resolve the promise as soon as a block containing your transaction arrives. This reduces unnecessary RPC calls and lets you react to chain reorganizations by resubscribing after a newHeads event signals a change.
// Requires a WS endpoint, e.g., wss://mainnet.infura.io/ws/v3/
const web3Ws = new Web3(new Web3.providers.WebsocketProvider('wss://...'));
async function waitForReceiptSubscription(txHash) {
return new Promise((resolve, reject) => {
const subscription = web3Ws.eth.subscribe('newHeads', (error, result) => {
if (error) return reject(error);
web3Ws.eth.getTransactionReceipt(txHash, (err, receipt) => {
if (err) return reject(err);
if (receipt) {
subscription.unsubscribe();
resolve(receipt);
}
// else keep waiting for the next head
});
});
// Safety timeout
setTimeout(() => {
subscription.unsubscribe();
reject(new Error('Subscription timeout'));
}, 30000);
});
}
// Example usage
const tx = await myToken.methods.transfer(recipient, amount)
.send({ from: senderAddress, provider: web3Ws });
const receipt = await waitForReceiptSubscription(tx.transactionHash);
console.log('Status from subscription:', receipt.status);
Permissions: Same as polling; additionally you need a WS/IPC endpoint that allows subscriptions (most public Infura/Alchemy WS endpoints do). Risks: Connection drops require reconnection logic; during a reorg you may receive the same log twice unless you filter by block number or transaction hash.
Trade‑off summary
| Aspect | Polling | Subscription |
|---|---|---|
| Compatibility | Works over HTTP, IPC, WS | Requires WS/IPC |
| Node load | Higher (repeated RPC calls) | Lower (event‑driven) |
| Latency | Up to polling interval | Near‑real‑time (next block) |
| Complexity | Simple loop | Needs reconnection, duplicate‑log handling |
Actionable guidance
- If your script runs in a browser or a environment that only offers HTTP/HTTPS RPC (e.g., MetaMask, simple backend), implement the polling loop with a reasonable interval (2‑5 s) and a timeout aligned with the expected block time.
- If you have a dedicated WS/IPC connection (Infura/Alchemy WS, a local node with WS enabled), prefer the subscription method. Add a reconnection wrapper that resubscribes on
errororcloseevents and discard logs whoseblockNumberis lower than the last processed block to avoid duplicates during reorgs. - Always check
receipt.statusafter you obtain the receipt; a mined transaction can still revert, and relying onblockNumberalone would give a false sense of success. - Validate the receipt in a testnet (e.g., Goerli or a local Hardhat node) by sending both a successful transfer and a failing one (insufficient balance) and confirming that
statusflips accordingly.
By matching the confirmation technique to your connection capabilities and latency requirements you reduce unnecessary RPC load while still getting reliable transaction outcomes.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.