Separate HTTP and WebSocket Providers in Web3.js v4 for Reliable Reads and Events
Use HttpProvider for eth_call reads and WebSocketProvider for log subscriptions in web3.js v4. Bind the ABI once with web3.eth.Contract and route reads and events separately for reliability.
28 Jul 2026, 11:31 UTC

A dashboard that polls a contract for balances and also needs live updates for transfers will feel broken if you use one provider for everything. Reads need low-latency, stateless calls. Events need a persistent stream. In web3.js v4 the practical decision is to bind the ABI once with web3.eth.Contract and then route reads over HttpProvider and subscriptions over WebSocketProvider.
Why a single provider creates two different failures
HttpProvider is perfect for eth_call. It is stateless, cheap to scale, and works behind firewalls. It cannot subscribe. Trying web3.eth.subscribe with an HTTP endpoint fails because the JSON-RPC subscription methods require a WebSocket transport.
WebSocketProvider can subscribe to logs and newHeads, but keeping a long-lived socket open for every read amplifies reconnect churn and rate limits. Separating concerns avoids that coupling.
ABI binding and the read path with eth_call
web3.eth.Contract(abi, address) binds an ABI to an address. The instance exposes contract.methods.<fn>() which encodes calls using the ABI. For view and pure functions you want local execution without gas.
In v4 the read pattern is:
// Node.js, requires an RPC URL and a valid ABI
import { Web3, HttpProvider } from 'web3';
const http = new HttpProvider('https://mainnet.example/rpc');
const web3 = new Web3(http);
const contract = new web3.eth.Contract(ABI, '0xContractAddress');
const balance = await contract.methods.balanceOf('0xUser').call();This maps to eth_call under the hood. Expected check: the returned value matches the on-chain state for the block the node is serving. Risk: a mismatched or incomplete ABI will encode the call incorrectly and decode the return data incorrectly, often without an explicit error.
Event access with logs and subscriptions
Real-time access uses web3.eth.subscribe('logs', filter) over a WebSocketProvider. Historical access uses web3.eth.getPastLogs(filter).
A minimal v4 setup with two providers:
import { Web3, HttpProvider, WebSocketProvider } from 'web3';
const http = new HttpProvider(process.env.HTTP_RPC_URL);
const ws = new WebSocketProvider(process.env.WS_RPC_URL);
const web3Read = new Web3(http);
const web3Stream = new Web3(ws);
const contract = new web3Read.eth.Contract(ABI, CONTRACT_ADDRESS);
const logFilter = {
address: CONTRACT_ADDRESS,
topics: [web3Read.utils.sha3('Transfer(address,address,uint256)')]
};
const subscription = web3Stream.eth.subscribe('logs', logFilter);
subscription.on('data', log => {
// Decode log.data and log.topics against the ABI event definition
});
subscription.on('error', err => {
// Handle reconnect logic here
});Where to run: a backend service or worker with network access to both RPC endpoints. Required permissions: a read-only RPC key for HTTP, and a WebSocket-capable endpoint that allows subscriptions. Do not use a private key in this read-only path.
For backfill, getPastLogs is bounded by node retention and RPC max block range. Very large fromBlock to toBlock windows can be truncated or rejected. A practical way to check the result is to request a small window covering a known test transaction, decode the log with the contract ABI, and verify topics and data match the emitted event.
PromiEvent for mutating calls
When you do need to send, contract.methods.mutatingFn().send({ from }) returns a PromiEvent. It emits pending, transactionHash, receipt and error, allowing progress tracking without manual polling. This still uses the provider you instantiated the Web3 instance with, so keep sends on a provider that has account access configured.
Trade-offs and verification
Limitations to plan for:
- Provider capability mismatch: HttpProvider cannot subscribe.
- getPastLogs range limits and node pruning can truncate history.
- ABI fidelity is critical for correct encoding and decoding.
- v4 changed imports and provider construction versus v1.x; code written for v1.x will not run unchanged.
Verification steps you can run against a local EVM node:
- Deploy a contract with a view function and an event.
- Instantiate web3.eth.Contract with its ABI and address, call the view method with .call(), and compare the result to on-chain state.
- Connect via WebSocketProvider, subscribe to logs with an address filter, emit the event in a test transaction, and confirm the subscription callback receives the log.
- Use getPastLogs with fromBlock and toBlock covering the test transaction, decode with the ABI, and verify topics and data.
Separate providers give you predictable reads and durable event streams without coupling their failure modes.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.