Solana

Solana Beta is live. Try BoltRPC Solana endpoints free - start your trial now.

How to Listen for Ethereum Events with WebSocket and ethers.js v6

Real-time Ethereum event listening with ethers.js v6 and WebSocket. Covers contract events, new block subscriptions, log filters, reconnection handling and production patterns.

BoltRPC
BoltRPC Team
8 min read
How to Listen for Ethereum Events with WebSocket and ethers.js v6

How to Listen for Ethereum Events with WebSocket and ethers.js v6

Polling for blockchain events with repeated HTTP calls works, but it is slow, expensive, and misses events that happen between polls. WebSocket subscriptions solve this: the node pushes events to your application the moment they occur.

This guide covers everything you need to build a reliable real-time event listener on Ethereum using ethers.js v6 and WebSocket.


Why WebSocket Instead of HTTP Polling

HTTP polling:

// Polling every 3 seconds — slow and expensive
setInterval(async () => {
  const events = await contract.queryFilter('Transfer', lastBlock, 'latest');
  // Process events
}, 3000);

Problems: you miss events between polls, you make unnecessary requests when nothing happens, your bill grows linearly with polling frequency.

WebSocket subscription:

// Events pushed to you the moment they occur — zero polling
contract.on('Transfer', (from, to, value) => {
  // Called immediately when a Transfer event is emitted
});

The node does the work. Your application receives a push notification for each matching event, in real time, with no polling overhead.


Setting Up a WebSocket Provider

import { ethers } from 'ethers';

const wsProvider = new ethers.WebSocketProvider(
  'wss://eu.endpoints.matrixed.link/ws/ethereum?auth=YOUR_KEY'
);

// Verify connection
wsProvider.on('block', (blockNumber) => {
  console.log('Connected. Current block:', blockNumber);
});

WebSocket connections are persistent. Unlike HTTP, the connection stays open and the node streams events to you as they occur.


Subscribing to New Blocks

wsProvider.on('block', async (blockNumber) => {
  console.log('New block:', blockNumber);

  // Optionally fetch full block data
  const block = await wsProvider.getBlock(blockNumber);
  console.log('Transactions in block:', block.transactions.length);
  console.log('Timestamp:', new Date(block.timestamp * 1000).toISOString());
});

When to use: Any application that needs to react to new blocks: dashboards displaying live block data, applications that check state after each block, countdown timers based on block height.


Listening to Contract Events

Basic Event Subscription

const ERC20_ABI = [
  'event Transfer(address indexed from, address indexed to, uint256 value)',
  'event Approval(address indexed owner, address indexed spender, uint256 value)',
];

const USDC_ADDRESS = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48';

const usdc = new ethers.Contract(USDC_ADDRESS, ERC20_ABI, wsProvider);

// Listen for all Transfer events
usdc.on('Transfer', (from, to, value, event) => {
  console.log('Transfer detected:');
  console.log('  From:', from);
  console.log('  To:', to);
  console.log('  Amount:', ethers.formatUnits(value, 6), 'USDC');
  console.log('  TX hash:', event.log.transactionHash);
  console.log('  Block:', event.log.blockNumber);
});

Filtering Events by Address

Use indexed parameters to filter for specific addresses:

const MY_ADDRESS = '0xYourWalletAddress';

// Only listen for transfers TO your address
const filter = usdc.filters.Transfer(null, MY_ADDRESS);

usdc.on(filter, (from, to, value, event) => {
  console.log(`Received ${ethers.formatUnits(value, 6)} USDC from ${from}`);
});
// Only listen for transfers FROM a specific address
const whaleFilter = usdc.filters.Transfer(WHALE_ADDRESS, null);

usdc.on(whaleFilter, (from, to, value) => {
  console.log(`Whale moved ${ethers.formatUnits(value, 6)} USDC`);
});

Listening to Multiple Events

// Listen for both Transfer and Approval
usdc.on('Transfer', (from, to, value) => {
  console.log('Transfer:', ethers.formatUnits(value, 6), 'USDC');
});

usdc.on('Approval', (owner, spender, value) => {
  console.log('Approval:', owner, 'approved', spender, 'for', ethers.formatUnits(value, 6), 'USDC');
});

Listening to DeFi Events

Uniswap V3 Swap Events

const UNISWAP_POOL_ABI = [
  'event Swap(address indexed sender, address indexed recipient, int256 amount0, int256 amount1, uint160 sqrtPriceX96, uint128 liquidity, int24 tick)',
];

const ETH_USDC_POOL = '0x88e6A0c2dDD26FEEb64F039a2c41296FcB3f5640';

const pool = new ethers.Contract(ETH_USDC_POOL, UNISWAP_POOL_ABI, wsProvider);

pool.on('Swap', (sender, recipient, amount0, amount1, sqrtPriceX96) => {
  // amount0 = ETH (18 decimals), amount1 = USDC (6 decimals)
  const ethAmount = ethers.formatEther(amount0 < 0n ? -amount0 : amount0);
  const usdcAmount = ethers.formatUnits(amount1 < 0n ? -amount1 : amount1, 6);

  console.log(`Swap: ${ethAmount} ETH ↔ ${usdcAmount} USDC`);
});

Aave Liquidation Events

const AAVE_POOL_ABI = [
  'event LiquidationCall(address indexed collateralAsset, address indexed debtAsset, address indexed user, uint256 debtToCover, uint256 liquidatedCollateralAmount, address liquidator, bool receiveAToken)',
];

const pool = new ethers.Contract(AAVE_POOL_ADDRESS, AAVE_POOL_ABI, wsProvider);

pool.on('LiquidationCall', (collateral, debt, user, debtAmount, collateralAmount, liquidator) => {
  console.log('Liquidation detected:');
  console.log('  User liquidated:', user);
  console.log('  Liquidator:', liquidator);
  console.log('  Debt covered:', ethers.formatUnits(debtAmount, 18));
});

Raw Log Subscriptions

For listening to events across multiple contracts or without an ABI, use raw log filters:

// Listen for ALL Transfer events on any ERC-20 contract
const transferTopic = ethers.id('Transfer(address,address,uint256)');

wsProvider.on({ topics: [transferTopic] }, (log) => {
  console.log('ERC-20 Transfer on contract:', log.address);
  console.log('TX hash:', log.transactionHash);
});

// Filter by specific contract + topic
wsProvider.on({
  address: USDC_ADDRESS,
  topics: [transferTopic]
}, (log) => {
  // Only USDC transfers
  const iface = new ethers.Interface(ERC20_ABI);
  const decoded = iface.parseLog(log);
  console.log('From:', decoded.args.from);
  console.log('To:', decoded.args.to);
  console.log('Value:', ethers.formatUnits(decoded.args.value, 6));
});

Handling Reconnection: The Production Pattern

WebSocket connections drop. Your provider disconnects, the node restarts, a network hiccup occurs. Without reconnection logic, your event listener goes silent and you never know it happened.

This is the most important thing in this guide.

import { ethers } from 'ethers';

const WSS_URL = 'wss://eu.endpoints.matrixed.link/ws/ethereum?auth=YOUR_KEY';

let provider;
let contract;

function setupListeners(provider, contractAddress, abi) {
  const c = new ethers.Contract(contractAddress, abi, provider);

  c.on('Transfer', (from, to, value, event) => {
    console.log('Transfer:', ethers.formatUnits(value, 6), 'USDC');
  });

  return c;
}

async function connect() {
  console.log('Connecting to WebSocket...');

  provider = new ethers.WebSocketProvider(WSS_URL);

  // Set up your listeners
  contract = setupListeners(provider, USDC_ADDRESS, ERC20_ABI);

  // Handle connection close
  provider.websocket.on('close', () => {
    console.log('WebSocket closed. Reconnecting in 5s...');

    // Remove old listeners to prevent memory leaks
    contract.removeAllListeners();

    setTimeout(connect, 5000);
  });

  // Handle errors
  provider.websocket.on('error', (error) => {
    console.error('WebSocket error:', error.message);
  });

  console.log('Connected. Listening for events...');
}

// Start
connect();

Exponential Backoff on Reconnect

For production, use exponential backoff rather than a fixed delay. Avoids hammering the provider if it is under load:

let reconnectDelay = 1000; // Start at 1 second
const MAX_DELAY = 30000;   // Max 30 seconds

async function connectWithBackoff() {
  try {
    await connect();
    reconnectDelay = 1000; // Reset on success
  } catch (error) {
    console.error(`Connection failed. Retrying in ${reconnectDelay / 1000}s...`);

    setTimeout(connectWithBackoff, reconnectDelay);

    // Double the delay for next attempt, capped at max
    reconnectDelay = Math.min(reconnectDelay * 2, MAX_DELAY);
  }
}

connectWithBackoff();

Cleaning Up Listeners

Every listener you add holds a reference. In long-running applications, failing to remove listeners causes memory leaks and duplicate event processing.

// Remove a specific listener
const handler = (from, to, value) => console.log('Transfer:', value);
contract.on('Transfer', handler);

// Later, remove just this handler
contract.off('Transfer', handler);

// Remove all listeners for a specific event
contract.removeAllListeners('Transfer');

// Remove ALL listeners on the contract
contract.removeAllListeners();

// One-time listener — automatically removed after first event
contract.once('Transfer', (from, to, value) => {
  console.log('First transfer received:', value);
});

Getting Past Events + Listening for New Ones

A common pattern: load historical events first, then switch to real-time listening. This avoids missing events that occurred before your WebSocket connected.

async function catchUpAndListen(contract, eventName, fromBlock) {
  // Step 1: Get all past events from fromBlock to now
  const currentBlock = await provider.getBlockNumber();
  const pastEvents = await contract.queryFilter(eventName, fromBlock, currentBlock);

  console.log(`Loaded ${pastEvents.length} past events`);
  pastEvents.forEach(event => processEvent(event));

  // Step 2: Subscribe to new events from current block onward
  contract.on(eventName, (args, event) => {
    if (event.log.blockNumber > currentBlock) {
      processEvent(event);
    }
  });

  console.log('Now listening for new events...');
}

await catchUpAndListen(usdc, 'Transfer', startBlock);

WebSocket vs HTTP: When to Use Each

Use CaseHTTPWebSocket
Read wallet balance
Submit transaction
Real-time block notifications
Contract event stream
Pending transaction monitoring
One-off contract reads✅ PreferredPossible
High-frequency polling❌ Expensive✅ Better

Rule of thumb: use HTTP for requests, WebSocket for subscriptions. If you are polling for changes more than once every few seconds, switch to WebSocket.


BoltRPC WebSocket Endpoints

BoltRPC supports WebSocket on all 20+ networks. Same API key works across all chains:

// Ethereum mainnet
wss://eu.endpoints.matrixed.link/ws/ethereum?auth=YOUR_KEY

// Arbitrum
wss://eu.endpoints.matrixed.link/ws/arbitrum?auth=YOUR_KEY

// Base
wss://eu.endpoints.matrixed.link/ws/base?auth=YOUR_KEY

// Polygon
wss://eu.endpoints.matrixed.link/ws/polygon?auth=YOUR_KEY

// Optimism
wss://eu.endpoints.matrixed.link/ws/optimism?auth=YOUR_KEY

Trusted by Chainlink, Tiingo, Gains Network, Enjin. Built on ISO/IEC 27001:2022 certified infrastructure via Matrixed.Link.

Start your free 2-week trial: trial.boltrpc.io


FAQ

Why does my event listener stop working after a while?

The WebSocket connection dropped and was not reconnected. Add reconnection logic as shown above. Without it, your listener silently goes offline and you will not know until you check manually.

Can I listen to events on multiple contracts at once?

Yes. Create a Contract instance for each contract and add listeners to each. All subscriptions share the same WebSocket connection. You are not opening a new connection per contract.

What is the difference between contract.on() and provider.on() with a filter?

contract.on('EventName', handler) uses your ABI to decode event arguments automatically. provider.on(filter, handler) gives you raw log objects that you decode manually. Use contract.on() when you have the ABI. Use provider.on() for cross-contract monitoring or when the ABI is unknown.

How many subscriptions can I have on one WebSocket connection?

This depends on your provider’s limits. Most providers support tens of concurrent subscriptions per connection. For very high numbers (hundreds of subscriptions), consider whether a polling approach with eth_getLogs might be more efficient for some use cases.

Does WebSocket work on all EVM chains?

Yes, with BoltRPC WebSocket is available on all supported networks except Hyperliquid. The subscription methods (eth_subscribe) are part of the standard EVM JSON-RPC spec supported across all EVM-compatible chains.

What happens to missed events during a reconnect?

Events that occurred while your connection was down are not pushed retroactively. After reconnecting, use contract.queryFilter(event, lastKnownBlock, currentBlock) to fetch any events you missed during the disconnection window, then resume live listening.

Frequently asked questions

Ready to build with high-performance RPC?

Start your free trial today. No credit card required. Access 20+ networks instantly.

Disclaimer: The content in this article is for informational purposes only and does not constitute financial, legal, or technical advice. Code examples and configurations are provided as-is. Always verify information with official documentation and test thoroughly in your own environment before deploying to production.

Continue reading