blockchainsv
Web3 Integration & APIs·August 11, 2026·19 min read

WebSocket connections in Web3: why HTTP polling persists

A WebSocket connection terminates the moment its underlying TCP socket closes. The more dangerous failure is the one that goes undetected — when the client continues to trust a stream that has…

WebSocket connections in Web3: why HTTP polling persists

A WebSocket connection terminates the moment its underlying TCP socket closes. The more dangerous failure is the one that goes undetected — when the client continues to trust a stream that has already stopped delivering state, and the gap between the two states is invisible to the application.

That distinction explains much of the debate around web3 provider websocket vs http polling. WebSockets can reduce latency for repeated reads, writes, and event delivery. The measured advantage is often around 10% to 20% compared with HTTP RPC for workloads that keep the channel busy. But the advantage exists only while the connection is alive, synchronized, authorized, and correctly resubscribed after interruption.

In production, that is a large condition set.

HTTP polling has a slower execution path. It also has fewer hidden states. Each request starts from an explicit provider interaction. The client can retry it. A proxy can cache it. A load balancer can route it. A firewall usually allows it. No heartbeat is required to determine whether the next request can be sent.

This is why HTTP polling remains the default transport for many Web3 libraries and infrastructure stacks. It is not the fastest mechanism in every case. It is the transport with the smaller operational attack surface.

The Fragility of Real-Time Streams: Why WebSockets Fail in Production

A WebSocket connection begins with an HTTP upgrade handshake. After the upgrade, the application is no longer making independent HTTP requests. It is maintaining a long-lived TCP session through several layers:

1. The browser or Node.js runtime.

2. The local network.

3. A NAT device or corporate proxy.

4. A load balancer or API gateway.

5. The RPC provider.

6. The provider's internal connection manager.

7. The node serving the subscription.

Every layer can terminate the session. Not all layers report the termination cleanly.

Network instability is the obvious failure mode. It is not the most dangerous one. The more common production defect is a silent disconnect. The client still holds a WebSocket object. The object has not emitted a useful error. The application assumes the subscription is active. New blocks arrive on-chain. No event reaches the frontend.

The contract state has mutated. The application state has not.

That is a failed synchronization invariant.

A robust WebSocket implementation must therefore maintain at least four conditions:

  • The socket is physically connected.
  • The RPC provider still accepts the session.
  • The subscription still exists on the provider.
  • The application has processed every relevant block or has a valid recovery path.

A TCP connection proves only the first condition. Sometimes it does not even prove that.

The reconnection problem is not one problem

A reconnect handler is not a complete solution. It handles transport recovery. It does not automatically repair application state.

Consider an event subscription for a token contract. The client receives events through block 18,500,000. The connection drops. The client reconnects after block 18,500,008. It creates a new subscription. The provider begins streaming new events from the current head.

What happened to blocks 18,500,001 through 18,500,008?

If the application does not query that range explicitly, the events are missing. The interface may display an incorrect balance, an incomplete transaction history, or a stale order status. The connection is healthy again. The data is still wrong.

This creates two separate recovery paths:

  • Transport recovery: establish a new WebSocket session and resubscribe.
  • State recovery: identify the last processed block and backfill the missing range.

The second path is where many implementations fail.

A correct event listener should persist a cursor. The cursor can be a block number, a block hash, or a more structured checkpoint depending on the reorganization model. On reconnect, the client must compare the cursor with the current chain head and replay the missing interval through an RPC query or an indexing layer.

The event stream is not the source of truth. The chain is.

Heartbeats expose a hidden state machine

Long-lived connections require liveness detection. A heartbeat usually consists of ping and pong messages, or an application-level request that expects a response within a deadline.

The implementation needs to distinguish between:

  • A connection that is open and responding.
  • A connection that is open but not delivering application messages.
  • A connection that is blocked by an intermediary.
  • A connection that has been closed locally but not cleaned up in all application components.
  • A connection that has reconnected without restoring subscriptions.

Each state needs a deterministic transition. Otherwise, a reconnect loop can produce duplicate subscriptions, duplicate event processing, or an uncontrolled number of sockets.

The common sequence is predictable:

1. The socket drops.

2. The error handler schedules a reconnect.

3. The previous listener remains registered.

4. The new socket is created.

5. The listener is registered again.

6. A later event is processed twice.

7. A state mutation is applied twice.

For a read-only UI, this may produce a duplicate notification. For an automated execution service, it can trigger duplicate transactions or duplicate off-chain accounting entries.

Exponential backoff reduces connection pressure. It does not repair idempotency. Event handlers still need a deduplication strategy. The event identity may include the transaction hash, log index, block hash, and contract address. A transaction hash alone is insufficient when a transaction emits multiple logs of the same event.

A WebSocket gives you a stream. It does not give you delivery guarantees, replay, or a correct state cursor.

Reorganizations make naive subscriptions unsafe

The EVM chain is not an append-only database from the application's perspective. A block can be replaced during a reorganization. A log observed in one branch may not exist in the canonical branch.

A subscription consumer that immediately commits every event to an irreversible database is making a finality assumption. That assumption may be valid for some applications and unacceptable for others.

The recovery design must specify:

  • How many confirmations are required before an event is treated as final.
  • Whether removed logs are handled.
  • Whether the data store can reverse a previous state mutation.
  • Whether the application reconciles against canonical block hashes.
  • What happens when the provider reconnects to a different chain tip.

HTTP polling does not remove reorganization risk. It makes the recovery operation easier to reason about because the client can query a defined block range. The transport does not dictate the consistency model. The application does.

Stateless Resilience: The Operational Case for HTTP Polling

HTTP polling is frequently dismissed as inefficient because it repeats requests. That description is incomplete. Repetition is also the mechanism that makes the transport recoverable.

A request to an HTTP JSON-RPC endpoint is self-contained. It includes the method, parameters, and authorization context required for the provider to execute it. If the request fails, the client can retry. If the process restarts, no subscription state has to be reconstructed before the next read.

For common dApp operations, that property matters more than a small latency reduction:

  • Fetching a wallet balance.
  • Reading token metadata.
  • Loading a historical transaction.
  • Estimating gas.
  • Querying a contract view.
  • Checking the latest finalized block.
  • Retrieving logs for a bounded block range.

These are discrete reads. They do not need a persistent stream.

HTTP also fits existing infrastructure. CDNs and reverse proxies can cache frequently requested data that changes slowly. Historical metadata, token lists, ABI-related resources, and finalized chain queries may benefit from caching. WebSockets do not provide the same caching model because the data is delivered over a live session rather than as independently addressable responses.

The benefit is not merely performance. It is failure containment.

If one HTTP request fails, the failure is local to that operation. If a WebSocket session fails, every subscription attached to the session may become stale at once.

Polling interval is a consistency decision

A polling interval is often configured as a performance parameter. It is more accurately a data freshness policy.

In ethers.js, the default polling interval for checking new blocks or events is 4,000 milliseconds. That does not mean the application has a four-second guarantee for every event. Provider load, response time, chain progression, batching, and client scheduling affect the result. It means the provider checks on that cadence by default.

Reducing the interval increases request volume. It does not automatically produce a better system.

Suppose a frontend polls every second for a balance that changes only after a user transaction. The additional requests may not improve the user's observed state if the transaction has not been mined. If the provider rate-limits the client, the higher polling frequency can reduce reliability precisely when the application needs confirmation.

A sound polling policy separates data classes:

Data typeSuitable transportReason
Historical transactionsHTTP requestThe query is bounded and independently retryable
Wallet balanceHTTP polling or targeted refetchThe value changes discretely and does not require a permanent stream
New block notificationsWebSocket or pollingLatency matters, but missed blocks can be recovered
Contract event historyHTTP log queries or indexerRange-based replay is required for correctness
Live trading or liquidation signalsWebSocket with HTTP reconciliationLow latency matters, but the stream cannot be trusted alone
Token metadataHTTP with cachingThe data changes slowly and benefits from cache layers

The correct choice is often hybrid. Use the lowest-complexity transport that satisfies the freshness requirement. Add a faster path only where latency has measurable product or execution value.

Polling can be wasteful. It can also be bounded.

The weak implementation polls everything at one interval. The stronger implementation defines a refresh policy per resource.

For example:

  • Poll the latest block while a transaction is pending.
  • Stop polling after the transaction reaches the required confirmation depth.
  • Refetch a balance after a known state-changing transaction.
  • Query logs by block range rather than repeatedly loading the entire history.
  • Increase the interval when the page is backgrounded.
  • Use a cache for immutable or finalized data.

This turns polling into a controlled read model rather than a blind loop.

The same principle applies to backend services. A worker can maintain a last-seen block and request logs from lastSeen + 1 to the current safe head. If the request fails, the worker retries the same range. If the process restarts, it resumes from the persisted cursor.

The operation must be idempotent. The worker should tolerate replay. That is the relevant invariant:

Every canonical block range is eventually processed exactly once in effect, even if it is requested more than once.

HTTP makes that pattern natural. WebSockets can participate in it, but they do not replace it.

Library Defaults and Developer Experience in ethers.js and viem

Library defaults reveal the assumptions most developers eventually encounter in production.

ethers.js supports both HTTP and WebSocket providers. Its polling model remains central because many provider actions are request-response operations. A JsonRpcProvider can retrieve state without maintaining a session. A WebSocket provider can receive pushed events, but the application assumes responsibility for connection lifecycle and recovery behavior. The library also exposes the provider.websocket accessor pattern in recent versions, which keeps the public interface familiar even when the underlying transport changes.

The distinction is visible in the API surface:

  • HTTP providers issue independent JSON-RPC requests.
  • WebSocket providers maintain a persistent transport.
  • Event listeners attached to a WebSocket depend on subscription continuity.
  • Polling-based listeners discover changes by checking chain state repeatedly.
  • The polling interval can be customized through the provider configuration.

The API does not remove the underlying failure modes. A convenience event listener is still an event consumer. It still needs a response to missed blocks, duplicate logs, reorgs, and provider termination.

With viem, actions such as watchBlocks and watchEvent use polling for non-WebSocket clients by default. A WebSocket client can also be configured to poll explicitly with the poll option. This is a practical acknowledgment that transport selection and event semantics are separate concerns. A developer may need WebSocket-like responsiveness while still preferring polling's recovery characteristics. The library allows that choice without forcing the application into a persistent transport.

The choice also matters for ethers js websocket provider reconnect behavior. ethers.js exposes WebSocketProvider reconnect logic through the standard WebSocket event surface — onopen, onclose, onerror, and the readyState property — but it does not automatically resubscribe to active filters or event topics. The application must keep track of every active subscription and reissue it after the new socket opens. Without that, the reconnect succeeds at the transport layer and silently fails at the application layer.

The provider abstraction does not equal provider equivalence

A shared method name can hide different operational behavior.

Two providers may both expose watchEvent. One may use an RPC node's native subscription. Another may poll eth_getLogs. A third may route requests through an indexing service. Their latency, ordering, reorg behavior, retention, and rate limits will differ.

The application needs to know which properties it depends on:

  • Does the provider guarantee ordered delivery?
  • Can it replay missed logs?
  • Does it surface removed logs?
  • Does it preserve subscriptions across backend failover?
  • Are historical queries limited by block range?
  • Does it enforce a maximum subscription duration?
  • How are rate limits applied to polling and streaming separately?
  • What happens when the chain endpoint changes during a reconnect?

An abstraction that hides these details can improve developer speed while obscuring the attack surface. That is acceptable only when the application has an explicit reconciliation layer.

React integration introduces a second lifecycle

Frontend Web3 integrations add a separate source of defects. React components mount and unmount. Dependencies change. Wallet accounts switch. Chains change. Providers are replaced.

A subscription created in an effect must be removed in the cleanup path. Otherwise, a component can create multiple active listeners over its lifetime. The resulting behavior looks like a provider problem but is a client lifecycle defect.

The relevant state transitions include:

1. The component mounts.

2. The wallet address is undefined.

3. The address becomes available.

4. The chain ID changes.

5. The provider changes.

6. The component unmounts.

7. The listener must be removed from the old provider.

A listener tied to an old account is not harmless. It can update the current interface with data from the previous account. A listener tied to an old chain can display an event from the wrong network. The state is syntactically valid and semantically false.

Use a stable query key. Include the chain ID and account address. Cancel or invalidate stale requests. Treat transport changes as state changes, not as implementation details.

Infrastructure Constraints: RPC Limits and Firewall Interference

The web3 rpc websocket vs http decision is constrained by infrastructure before application code runs.

RPC providers commonly enforce limits on WebSocket sessions. These can include:

  • Maximum concurrent connections.
  • Maximum session duration.
  • Message throughput caps.
  • Subscription limits per connection.
  • Idle timeouts.
  • Restrictions on supported subscription types.
  • Provider-specific behavior during node failover.

The exact limits vary. They are not stable across providers or plans. Treating a WebSocket as a permanent private channel is therefore incorrect unless the limit policy has been read and accounted for in the client design.

Public RPC endpoints often favor HTTP because the connection profile is simpler to meter. A WebSocket session consumes an open connection slot for as long as it is alive, regardless of message activity. Idle connections still occupy capacity. A provider that wants to charge per request or per active session will favor a transport that exposes a countable unit of work. HTTP requests are easy to count. WebSocket minutes are harder to meter fairly because activity is internal to the session.

The same economics shape the free-tier limits most developers first encounter. A free plan may allow thousands of HTTP requests per minute but only a handful of concurrent WebSocket connections. The decision to use WebSockets on a free plan is therefore often also a decision to design around a small fixed connection budget.

Firewalls, proxies, and silent breakage

Corporate networks, mobile carriers, hotel Wi-Fi, and aggressive NAT devices all interfere with long-lived TCP sessions. Idle sessions are routinely dropped after a few minutes. Outbound connections on non-standard ports can be blocked outright. Transparent proxies can insert themselves between the client and the provider without informing either side.

These conditions are well known. The trap is assuming they are not present in production. Even a well-configured datacenter can route through a layer that does not support long-lived connections correctly. The application should treat the network as untrusted by default and design the transport accordingly.

A WebSocket that is healthy in development can be partially broken in production:

  • The socket opens.
  • The provider accepts the upgrade.
  • No application messages arrive.
  • Pings return irregularly or with unexpected latency.
  • Reconnect attempts succeed but the provider has already dropped the subscription.

From the client perspective, the connection looks alive. From the provider's perspective, the client has gone silent. The failure mode is not a thrown error but a divergence between two views of the same session.

HTTP requests rarely exhibit this profile. Each request is opened, executed, and closed within a predictable time window. A failed attempt produces an error code or a timeout that the application can handle. There is no parallel state to keep in sync.

Connection reuse and worker isolation

Backend services introduce another constraint. A Node.js worker handling many requests can share an HTTP provider across all of them. It cannot safely share a WebSocket provider across requests that are supposed to be independent, because subscription state is global to the socket.

Two requests that both subscribe to Transfer events on the same contract will receive every event in the same stream. If one request expects only events for address A and another expects only events for address B, the worker has to multiplex the stream and route each event back to the correct caller. The multiplexing logic, the backpressure handling, and the unsubscribe semantics become part of the application.

The alternative is one WebSocket per logical subscriber. That works for a small number of subscribers and breaks down quickly when subscriber count grows. The provider's connection cap is reached, requests start to fail, and the worker degrades into a polling implementation with extra steps.

This is one of the reasons most production backends settle on HTTP polling for log retrieval and reserve WebSocket usage for narrowly scoped high-frequency needs such as new block headers or mempool observation. The boundary is not technological. It is operational.

When to Choose Latency Over Reliability in dApp Architecture

The argument so far has emphasized reliability. That is not the whole picture. Latency matters in specific dApp categories, and pretending otherwise would distort the decision.

Real-time liquidation engines, cross-chain arbitrage bots, mempool sniping for token launches, and live trading interfaces on decentralized exchanges all benefit from sub-second state updates. A 200-millisecond advantage in observing a new block can be worth real money in adversarial environments where other participants are also racing the same transaction.

In these contexts, the WebSocket is not an optional optimization. It is part of the competitive surface. The application must still implement the recovery discipline described earlier, but the additional complexity is justified by the latency budget.

A useful test before choosing WebSocket is to ask three questions:

  • What is the financial or experiential cost of seeing a state change one block later?
  • Can the application survive a missed event without corrupting its internal state?
  • Is there a reconciliation mechanism that can repair any drift after a reconnect?

If the cost of delay is low, if missed events are easy to recover from, and if reconciliation already exists, HTTP polling will usually be the cleaner choice. If the cost of delay is high, if missed events are expensive to recover from, and if reconciliation has to be built anyway, a WebSocket-driven pipeline with a parallel HTTP reconciliation layer becomes the right structure.

Hybrid transport as a deliberate design

The mature pattern is not either-or. It is layered.

A typical architecture looks like:

1. An HTTP provider handles all read operations, balance queries, and historical log fetches.

2. A WebSocket provider streams only the high-frequency events the application cannot afford to miss.

3. A persistent cursor records the last processed block per subscription.

4. A reconciliation job periodically compares the application's state against an HTTP query for the same range.

5. On disconnect, the WebSocket reconnect is paired with a range query that backfills any gap.

This is more code than a single-transport implementation. It is also a system that can be reasoned about under partial failure. If the WebSocket is healthy, latency is low. If the WebSocket is unhealthy, the application degrades to polling without losing correctness.

The choice between transports is therefore less interesting than the choice of architecture. The architecture has to assume that any single transport can fail. It has to keep state on the chain, not on the wire. It has to define what finality means for the application and treat every block as provisional until that threshold is reached.

A practical decision rubric without the rubric

The temptation is to leave the reader with a numbered checklist. That is the wrong ending. The decisions involved are workload-specific and change with provider pricing, with new client libraries, and with new subscription types.

The pattern worth internalizing is shorter:

  • The chain is the source of truth. The stream is not.
  • A socket that looks open is not the same as a socket that is delivering data.
  • A reconnect that succeeds at the transport layer has not necessarily succeeded at the application layer.
  • A polling loop that is bounded, idempotent, and cursor-driven is harder to break than an unbounded one.
  • A hybrid transport with explicit reconciliation is more durable than any single transport.

These principles do not depend on a specific library, a specific RPC provider, or a specific chain. They are the operating assumptions that allow the rest of the implementation to remain correct when something goes wrong.

HTTP polling persists in Web3 not because developers lack the sophistication to use WebSockets. It persists because the discipline required to make a WebSocket correct in production is greater than the discipline required to make HTTP polling correct, and the marginal latency benefit is rarely large enough to justify the additional risk in applications where correctness matters more than speed.

FAQ

Why do WebSockets fail more often than HTTP in production?
WebSockets are long-lived TCP sessions that pass through multiple layers like proxies, load balancers, and NAT devices, any of which can silently terminate the connection without the client realizing the stream has stopped.
How can I ensure my application doesn't miss events during a WebSocket disconnect?
You must implement a state recovery path that tracks the last processed block via a cursor and queries the RPC provider for the missing range of blocks immediately upon reconnection.
Is HTTP polling always slower than using WebSockets?
WebSockets typically offer a 10% to 20% latency advantage for repeated reads and event delivery, but this benefit is only realized if the connection remains perfectly synchronized and active.
Why is a simple reconnect handler insufficient for WebSocket stability?
A reconnect handler only restores the transport layer; it does not automatically repair the application state, resubscribe to filters, or recover events that occurred while the connection was down.
What is the best way to handle duplicate events when using WebSockets?
You should implement a deduplication strategy that uses unique identifiers such as the transaction hash, log index, block hash, and contract address to ensure each event is processed exactly once.

By Caleb North