Listening to ERC‑20 Transfers in Real Time with Web3.js WebSocket Subscriptions
Learn how to use Web3.js to subscribe to ERC‑20 Transfer events over WebSocket, see a working example, understand trade‑offs, and keep your listener robust.
21 May 2026, 21:51 UTC

Why Do You Need a Real‑Time Listener?
Many dApps—price trackers, wallets, or automated market makers—must react instantly when a token moves. Polling the blockchain every few seconds is wasteful and can miss events that occur between polls. Web3.js offers a built‑in WebSocket subscription that pushes logs as soon as the node emits them, giving you true real‑time updates.
Thesis: Web3.js WebSocket Subscriptions Are the Fastest and Easiest Path, But Only If Your Provider Supports It
Web3.js can subscribe to the Transfer event of any ERC‑20 contract via the eth_subscribe RPC call. When the node emits a log that matches your filter, the callback runs instantly. However, not every provider offers a stable WebSocket endpoint, and long‑running listeners can consume memory if not handled properly.
1. How Web3.js Subscriptions Work
At the heart of the subscription is a JSON‑RPC request sent over a WebSocket connection:
{"jsonrpc":"2.0","id":1,"method":"eth_subscribe","params":["logs",{
"address":"0x6b175474e89094c44da98b954eedeac495271d0f",
"topics":["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a0e1f9c5b5"]
}]}
}
The filter object contains:
address– the token contract.topics[0]– the Keccak‑256 hash of the event signatureTransfer(address,address,uint256). The Web3.js helperweb3.utils.sha3can compute it.
Web3.js returns a subscription ID that you can later use to cancel the listener:
const subscription = await web3.eth.subscribe('logs', filter, callback);
// later
await subscription.unsubscribe();
2. A Concrete Worked Example
Below is a minimal script that connects to Infura’s mainnet WebSocket endpoint, subscribes to DAI Transfer events, and logs each one. Replace YOUR_PROJECT_ID with your Infura project ID.
const Web3 = require('web3');
const INFURA_WS = 'wss://mainnet.infura.io/ws/v3/YOUR_PROJECT_ID';
const web3 = new Web3(new Web3.providers.WebsocketProvider(INFURA_WS));
const DAI_ADDRESS = '0x6b175474e89094c44da98b954eedeac495271d0f';
const TRANSFER_SIG = web3.utils.sha3('Transfer(address,address,uint256)');
const filter = {
address: DAI_ADDRESS,
topics: [TRANSFER_SIG]
};
async function startListening() {
const subscription = await web3.eth.subscribe('logs', filter, (error, result) => {
if (error) {
console.error('Subscription error:', error);
return;
}
const from = '0x' + result.topics[1].slice(26);
const to = '0x' + result.topics[2].slice(26);
const value = web3.utils.hexToNumberString(result.data);
console.log(`Transfer: ${from} → ${to} (${value} DAI)`);
});
subscription.on('connected', () => console.log('Listening for Transfer events...'));
subscription.on('error', err => console.error('Subscription error event:', err));
}
startListening();
Run the script with node listen.js. Any DAI transfer that occurs after the script starts will trigger the callback immediately. If you want to test, send a small amount of DAI from a wallet you control and watch the console.
3. Trade‑offs and Limitations
While WebSocket subscriptions are instant, they come with caveats:
- Provider Support – Public nodes like Infura require a paid plan for continuous subscriptions. Free tiers may throttle or close idle connections.
- Connection Stability – WebSocket connections can drop. Your script must detect
closeevents and reconnect, otherwise you lose updates. - Memory Footprint – Each callback is queued in Node’s event loop. If logs arrive faster than you process them, the queue grows, potentially exhausting memory. Use
async/awaitor stream libraries (e.g., RxJS) to back‑pressure the flow. - Polling Fallback – If you connect to a provider that lacks WebSocket support, Web3.js automatically falls back to
eth_getLogspolling. This introduces latency and higher bandwidth usage.
4. Best Practices for a Robust Listener
- Reconnection Logic – Listen for
closeorerrorevents on the WebSocket provider and re‑establish the subscription after a short delay. - Back‑pressure – Process each log asynchronously; if you need to persist data, write to a queue or database in a separate worker.
- Monitoring – Expose a health endpoint that reports the last received block number. Compare it to
web3.eth.getBlockNumber()to detect missed logs. - Rate‑Limit Awareness – Keep track of the number of subscription messages you send. Public nodes may limit the number of active subscriptions per project.
- Version Check – Ensure you use Web3.js v1.7.5 or later; earlier releases use a different subscription API.
Actionable Closing
To add real‑time token transfer awareness to your dApp:
- Choose a WebSocket‑capable provider (Infura, Alchemy, or a local geth node with
--ws). - Implement the subscription code above, adding reconnection and back‑pressure handling.
- Deploy your listener as a small microservice; expose health metrics and logs for debugging.
- Monitor usage against your provider’s limits and scale up if needed.
By following these steps, you’ll have a reliable, low‑latency feed of ERC‑20 Transfer events that powers instant UI updates, automated trading, or auditing tools.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.