Efficient Event Handling in Web3.js: Subscriptions vs. Polling
Learn how to choose between Contract.events and eth.subscribe in Web3.js to build real-time dApps that are resource-efficient and resilient to connection drops.
16 Sept 2025, 03:45 UTC

Your dApp shows a token balance that updates several blocks after the transaction, or you notice your provider bill climbing because every log from a popular contract is being pulled. In Web3.js, the remedy is not to increase polling frequency but to choose the correct event-handling primitive and tighten the filter before data leaves the node.
Takeaway: Use Contract.events for typed, contract-specific streams and web3.eth.subscribe('logs') for generic, multi-contract filtering, both over a WebSocket provider. Always bind the query with topics and a narrow block range, add explicit reconnection handling, and fall back to getPastLogs for historical backfill.
Two Primitives, Different Costs
Web3.js (v1.x) exposes two primary mechanisms for monitoring the blockchain. Contract.events.<eventName>(options, callback) builds a filter from the contract ABI and is convenient when you know the contract address and event signature. In contrast, web3.eth.subscribe('logs', options, callback) is lower-level, works across any contract, and lets you specify raw topics directly.
Both require a WebSocket provider. HTTP providers are stateless and cannot maintain a subscription; they can only poll using eth.getPastLogs. While a persistent WebSocket connection delivers real-time updates with lower latency, it consumes more resources on both the client and the provider. HTTP polling is simpler to deploy but introduces latency and can overload a provider on high-frequency contracts.
Designing Filters to Avoid Data Loss
Filter options such as fromBlock, toBlock, address, and topics reduce bandwidth and processing overhead. However, incorrect configuration often leads to silent data loss.
- Topics: These are Keccak-256 hashes of the event signature and any indexed parameters. An incorrect encoding will match nothing, and Web3.js will not issue a warning. Always derive topics from the contract ABI (e.g., using
web3.utils.sha3) rather than hand-typing strings. - Block Ranges: Requesting a very large range from
fromBlocktotoBlockcan trigger provider throttling or timeouts. For historical backfill, paginate in chunks (e.g., 2,000–5,000 blocks per request) and advance the cursor from the last processed block. For live streams, start from'latest'and rely on the subscription for new blocks.
Worked Example: Tracking ERC20 Transfers
Run this script in a Node.js environment (v14+). You will need a WebSocket endpoint URL (such as Infura or Alchemy). No special OS permissions are required, but ensure your provider key is kept secure.
const Web3 = require('web3');
const WS_URL = 'wss://mainnet.infura.io/ws/v3/YOUR_PROJECT_ID';
const CONTRACT_ADDRESS = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'; // USDC
const ERC20_ABI = [{
anonymous: false,
inputs: [
{ indexed: true, name: 'from', type: 'address' },
{ indexed: true, name: 'to', type: 'address' },
{ indexed: false, name: 'value', type: 'uint256' }
],
name: 'Transfer',
type: 'event'
}];
const web3 = new Web3(new Web3.providers.WebsocketProvider(WS_URL));
const contract = new web3.eth.Contract(ERC20_ABI, CONTRACT_ADDRESS);
// Use the contract helper for typed event subscriptions
contract.events.Transfer({
fromBlock: 'latest'
}, (error, event) => {
if (error) {
console.error('Subscription error:', error);
return;
}
console.log('New Transfer detected:', event.returnValues);
});
Trade-offs and Stability
WebSocket subscriptions are fragile. Network blips or provider-side timeouts can drop the connection without triggering a JavaScript error immediately. To ensure production stability, you must implement reconnection logic that listens for 'error' and 'close' events on the WebSocket provider.
Furthermore, subscriptions only capture events from the moment they are established. To avoid gaps during a reconnection, your application should record the last processed block height and perform a getPastLogs query for the missing range before restarting the subscription.
Verification
To verify your implementation, connect to a testnet WebSocket endpoint and trigger a contract event. Confirm the callback executes in real-time. To test reliability, manually disconnect your network interface; the subscription should fail, and your reconnection logic should re-establish the stream without losing events that occurred during the downtime.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.