Efficiently Filtering Contract Events with Web3.js v1.x
Learn how to use Web3.js event subscriptions with indexed filters to listen only to the blockchain events you need, reducing node load and enabling real‑time updates.
05 Feb 2026, 16:19 UTC

Problem: Polling for Specific Events Wastes Resources
Many decentralized applications need to react to a particular contract event—for example, when a specific address receives an ERC‑20 token transfer. A naïve approach polls the node with getPastEvents every few seconds, retrieving all matching events and then discarding the unwanted ones. This generates unnecessary RPC traffic, increases latency, and can hit rate limits on public nodes.
Thesis: Use Web3.js Subscriptions with Indexed Filters for Real‑Time, Low‑Overhead Listening
Web3.js v1.x provides a subscription API that, when paired with a WebSocket provider, opens a persistent channel to the node. By supplying a filter object that matches indexed event parameters, the node returns only the events you care about. The subscription emits a data event for each matching log and an error for connection issues, allowing asynchronous handling via callbacks or async/await.
How Indexed Filtering Works
In Solidity, event parameters marked with indexed are stored in the transaction’s log topics, making them searchable by the Ethereum node. Web3.js translates a filter like { filter: { indexedParam: value } } into a topic filter that the node applies before sending data back. Non‑indexed fields cannot be filtered at the node level; they must be checked client‑side after receipt.
Building a Filter Object
fromBlock: start block number or'latest'to begin listening from the tip.toBlock: optional end block;'latest'tails new events.filter: an object where keys are the indexed parameter names (as defined in the event ABI) and values are the exact values to match.
Example filter for an ERC‑20 Transfer(address indexed _from, address indexed _to, uint256 _value) event that only cares about transfers to a specific address:
const filter = {
fromBlock: 'latest',
filter: { _to: '0xAbCdEf1234567890AbCdEf1234567890AbCdEf12' }
};
Worked Example: Listening for Filtered ERC‑20 Transfers
The following Node.js script demonstrates the full lifecycle: create a Web3 instance with a WebSocket endpoint, subscribe to the Transfer event with the filter above, handle incoming data, and cleanly unsubscribe on termination.
const Web3 = require('web3');
// Replace with your own WebSocket endpoint (Infura, Alchemy, Hardhat, etc.)
const WS_ENDPOINT = 'wss://mainnet.infura.io/ws/v3/YOUR_PROJECT_ID';
const CONTRACT_ADDRESS = '0xTokenContractAddressHere';
const ERC20_ABI = [
{
anonymous: false,
inputs: [
{ indexed: true, internalType: 'address', name: '_from', type: 'address' },
{ indexed: true, internalType: 'address', name: '_to', type: 'address' },
{ indexed: false, internalType: 'uint256', name: '_value', type: 'uint256' }
],
name: 'Transfer',
type: 'event'
}
];
async function main() {
const web3 = new Web3(new Web3.providers.WebsocketProvider(WS_ENDPOINT));
const contract = new web3.eth.Contract(ERC20_ABI, CONTRACT_ADDRESS);
const subscription = contract.events.Transfer({
fromBlock: 'latest',
filter: { _to: '0xAbCdEf1234567890AbCdEf1234567890AbCdEf12' }
}, (error, event) => {
if (error) {
console.error('Subscription error:', error);
return;
}
console.log('Matching Transfer event:', event.returnValues);
});
// Handle connection‑level errors
subscription.on('error', console.error);
// Graceful shutdown on SIGINT
process.on('SIGINT', async () => {
await subscription.unsubscribe();
await web3.currentProvider.disconnect();
console.log('Unsubscribed and disconnected.');
process.exit(0);
});
}
main().catch(console.error);
Where to run: Save the script as listen.js, install web3@^1.0.0 via npm, and execute node listen.js. You need network access to the WS endpoint and read‑only access to the contract (no private keys required).
Verification Steps
- Deploy a test ERC‑20 contract on a local Hardhat node that emits
Transferevents. - Run the script with a filter matching an address you control.
- Send a token transfer to that address; you should see a
datalog with the event details. - Send a transfer to a different address; no
datalog should appear. - Remove the filter (or set it to an empty object) and confirm that all transfers trigger the handler.
These steps confirm that the node‑side filter is working as expected.
Trade‑Offs and Limitations
- Indexed‑only filtering: If you need to filter on a non‑indexed field (e.g.,
_value), the node will return all events and your application must perform the extra check, which can increase bandwidth. - Node support: Some light‑client or custom node configurations may not implement efficient topic filtering, causing a fallback to client‑side filtering and higher load.
- Connection resilience: WebSocket connections can drop; you must handle the
errorevent and implement reconnection logic for production use.
Actionable Closing
To adopt this pattern in your project:
- Identify the indexed parameters of the event you care about.
- Construct a filter object with those parameters and a suitable
fromBlock. - Use
myContract.events.MyEvent(filter, callback)with a WebSocket provider. - Handle
datafor incoming events anderrorfor connection issues. - Unsubscribe when the listener is no longer needed to free server resources.
By leveraging Web3.js’s built‑in event filtering, you reduce unnecessary RPC calls, achieve near‑real‑time responsiveness, and keep your dApp’s infrastructure costs low.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.