Use HTTP for request-response RPC calls (reads, transaction sending) and WebSocket for real-time subscriptions (new blocks, logs, pending transactions). That rule-of-thumb covers most cases, but the details matter: WebSocket connections drop silently, HTTP retries are far easier, and providers meter the two differently. This guide walks through how each transport behaves, where each fails, and a decision framework so you choose per workload instead of picking one transport for your whole stack.

What WebSocket and HTTP RPC actually are

Ethers.js documentation card, the library that ships WebSocketProvider — official ethers.js image
Photo: ethers.js (official project imagery)

HTTP RPC is the ordinary web model: open a connection, send one JSON-RPC request, get one response, done. Every call is independent. There is no memory between requests, which is exactly why it is so robust — any request can be retried, load-balanced to a different server, or cached without ceremony. When you call eth_blockNumber or send a transaction with eth_sendRawTransaction, HTTP treats it like any other web request.

WebSocket RPC upgrades an HTTP connection into a persistent, two-way channel. Both sides can send messages at any time, which enables the one thing HTTP fundamentally cannot do: the server pushing data to you without being asked. You can still make ordinary request-response calls over a WebSocket, but its real purpose is subscriptions.

The subscription model is standardized in Ethereum clients as eth_subscribe. Geth's documentation lists four subscription types — newHeads ("fires a notification each time a new header is appended to the chain"), logs (matching log events from new blocks), newPendingTransactions, and syncing — and states plainly that "subscriptions require a full duplex connection," offered over WebSocket and IPC. HTTP is not on the list, and cannot be: there is no channel for the server to push through.

So the technical split is clean. Request-response works on both transports. Push only works on WebSocket (or IPC, when you are on the same machine as the node — see our overview of blockchain RPC and node infrastructure for how these interfaces fit together).

It helps to see what a subscription looks like on the wire, because it explains everything about the failure modes later. You send one eth_subscribe call with the type you want and any filter parameters. The node replies once with a subscription ID. From then on, notifications tagged with that ID arrive whenever the event fires — you send nothing further. When the connection closes, for any reason, the node forgets the ID and everything about it. The subscription was never a standing order recorded somewhere durable; it was a property of that one socket. That is the entire reason reconnection logic must re-subscribe from scratch, and why no events are queued for you while you are away.

A note on the older middle ground: HTTP filters. Methods like eth_newFilter and eth_getFilterChanges let an HTTP client register a filter on the node and poll for accumulated changes — polling, but with the node doing the bookkeeping. They work, but they store per-client state on the node, many hosted providers restrict or discourage them behind load balancers (where your next poll may land on a server that never heard of your filter), and modern client libraries have largely moved on. For new code, the practical choice is plain polling or real subscriptions.

Where each transport breaks: HTTP vs WebSocket

QuickNode platform card from quicknode.com, which serves both HTTP and WebSocket RPC endpoints
Photo: QuickNode (official product imagery)

Choosing a transport is really choosing which failure modes you would rather handle.

HTTP's failure modes are boring, and that is its virtue. A request fails visibly: timeout, connection refused, or an error status. Because each request is independent, the fix is a retry with backoff, and any competent HTTP library ships one. Statelessness also means horizontal scale is trivial — a provider can spread your requests across a fleet, and your code cannot tell. The costs are latency overhead per request (connection setup, headers) and the polling problem: to learn about new events over HTTP you must ask repeatedly, and most polls return nothing. Polling loops are the single largest source of wasted RPC quota; if throttling is your current pain, the fixes in how to fix RPC rate limit errors address exactly that pattern.

WebSocket's failure modes are quiet, and that is its danger. A WebSocket that dies does not always announce it. Idle connections get culled by load balancers and NAT tables; a laptop sleeping and waking leaves the socket half-open; a provider restart drops every subscription on the node. The insidious part is what silence means: a subscription that has delivered nothing for two minutes is indistinguishable from a chain that produced no matching events — unless you check. Production WebSocket code therefore needs three things HTTP code does not:

  1. Heartbeats. Ping the connection (or watch newHeads, which should arrive roughly every 12 seconds on Ethereum mainnet) and treat prolonged silence as a dead socket.
  2. Reconnection with resubscription. Subscriptions are per-connection state. After reconnecting you must re-issue every eth_subscribe, and your code must remember what it subscribed to.
  3. Gap healing. Events that occurred while you were disconnected were never queued for you. On reconnect, fetch the block range you missed over ordinary request-response and replay it, or your event log has holes.

None of this is exotic — libraries like viem and ethers handle parts of it — but it is state you own. A team that is not ready to own it will ship a dapp that mysteriously stops updating until the tab is refreshed, which is the classic symptom of an unmonitored dead socket.

More in Node & RPC Infrastructure

Performance and metering differences: HTTP vs WebSocket RPC

QuickNode Streams card for pushing blockchain data over persistent connections — official QuickNode image
Photo: QuickNode (official product imagery)

For one-off calls, transport latency differences are small and usually dominated by the work the node does to answer. WebSocket avoids per-request connection overhead, which matters when you fire hundreds of sequential calls, but batching over HTTP closes much of that gap. The decisive performance difference is event delivery: a push arrives once, when something happens, while polling at interval T discovers events T/2 late on average and burns a request every interval regardless.

The arithmetic is stark. A frontend polling for new blocks every 2 seconds makes 43,200 requests a day, and on a 12-second block time about five of every six polls return nothing new. The equivalent newHeads subscription delivers roughly 7,200 messages a day — an 83% reduction in traffic, with better freshness. Multiply by every user tab you have open and the polling model is strictly worse for reactive data.

Latency composition differs too, in a way that matters for bots. Over HTTP, your effective event latency is poll interval plus request round-trip plus node processing. Over a subscription it is node processing plus one push, with no interval term at all. Shrinking a poll interval to chase latency just converts the interval term into wasted requests; the subscription removes the term entirely. That is why no amount of aggressive polling matches push for event-driven work — the models differ structurally, not by a constant factor.

Metering is the caveat. Providers count WebSocket usage too — typically per message and per concurrent connection, with separate caps on how many sockets one key may hold open. A quiet newHeads subscription is cheap. A newPendingTransactions firehose on a busy chain can deliver more messages per minute than your old polling loop made in an hour, and some subscription types are priced accordingly. Push replaces empty polls; it does not make high-volume data free.

Decision framework: WebSocket vs HTTP RPC per workload

viem TypeScript client card from viem.sh, the library behind webSocket transport
Photo: viem (official project imagery)

The right frame is per-workload, not per-app. Most real dapps use both transports at once, and the table shows the natural split. The "cost driver" column is the derived comparison that matters: what you actually pay for under each model.

WorkloadBest transportWhyCost driver
Wallet reads, balances, one-shot queriesHTTPStateless, retryable, cacheableRequests made
Sending transactionsHTTPMust not be lost to a silent socket drop; retries are criticalRequests made
Live UI updates (blocks, prices, positions)WebSocket newHeads + targeted readsOne push per block replaces constant pollingMessages received (~1 per block)
Event-driven bots and keepersWebSocket logs subscriptionReaction latency is the productMessages + reconnect engineering
Mempool watchingWebSocket newPendingTransactionsOnly exists as pushVery high message volume
Historical backfills and indexingHTTP request-responseBounded batch work, easy checkpointing and retryRequests, but batchable

Choose HTTP if the interaction is request-shaped: reads, writes, backfills, anything where retrying is more important than reacting. Choose WebSocket if the interaction is event-shaped and freshness is a feature: live dashboards, bots, anything currently implemented as a fast polling loop. Choose both if you are building a typical production dapp — HTTP for the read/write path, one shared WebSocket for reactivity — and route each call type deliberately. Skip WebSocket entirely if your app has no live-updating surface; a portfolio page users refresh manually gains nothing from a subscription infrastructure it must then babysit.

Setting it up without footguns: dual transport example

wagmi React hooks card from wagmi.sh, built on WebSocket-capable viem transports
Photo: wagmi (official project imagery)

A few implementation details separate a robust dual-transport setup from a flaky one.

Use different URLs with the same key. Providers expose both transports on parallel endpoints — an https:// URL and a wss:// URL for the same project key. Configure them as two named transports in your client library rather than deriving one from the other in code; the explicitness pays off when you later point one of them somewhere else.

Let your library multiplex. Modern clients like viem maintain one WebSocket and multiplex every subscription over it. Resist opening a socket per subscription or per component — providers cap concurrent connections, and each socket is another thing that can die silently. One connection, many subscriptions, one reconnect loop.

Make the heartbeat observable. Do not bury connection health in library internals. Surface "seconds since last block" as a metric or even in the UI footer. It is the single most informative health signal a chain-connected app has, it doubles as your dead-socket detector, and users of trading interfaces genuinely want to see it.

Decide transaction paths first. Whatever else you route over WebSocket, write down that eth_sendRawTransaction goes over HTTP with retries, and enforce it. The costliest WebSocket failure is not a stale chart — it is a user's signed transaction handed to a socket that was already dead, with no error surfaced until the confirmation never comes.

Test the ugly paths on purpose. Kill the socket mid-session in development: disconnect the network, restart the local node, sleep the laptop. Watch whether your app notices, reconnects, resubscribes, and heals the gap. Every one of those steps that fails silently in dev will fail silently in production, where you will not be watching.

Which setup fits your situation

Situation 1: A dapp frontend that feels stale without constant polling

Your UI polls every couple of seconds so balances and positions feel live, and the request volume is starting to throttle. Restructure around one newHeads subscription per client: when a block arrives, refetch just the values that could have changed, over ordinary HTTP. You get per-block freshness — there is nothing fresher to show, since state only changes with blocks — at one push message per 12 seconds plus a handful of targeted reads. Keep transaction sending on HTTP so a dropped socket can never eat a user's transaction.

Situation 2: A liquidation or arbitrage bot where reaction time is revenue

Your bot's edge is measured in blocks, sometimes in position-in-block. Subscribe to logs for the specific contracts and topics you act on, and to newHeads as both a clock and a heartbeat. Invest seriously in the reconnect-resubscribe-heal loop from the failure section, because for a bot a silently dead subscription is not a stale UI — it is unbounded downside while you believe you are watching. This is also the workload where colocating with your own node pays: IPC and local WebSocket remove the provider's queueing from your critical path.

Situation 3: An indexer backfilling history while staying current at the tip

You face both shapes at once: a bounded historical sweep and an unbounded live tail. Split them explicitly. The backfill is HTTP territory — batched eth_getLogs over checkpointed block ranges, retryable and resumable. The live tail is a logs subscription feeding the same pipeline. The subtle part is the seam: record the block height where your backfill ends, start the subscription first, buffer its output, then backfill up to the buffer and splice. Done in the other order, events landing during the gap belong to neither stream and vanish.

FAQ: WebSocket vs HTTP RPC

Can I make normal RPC calls over WebSocket instead of HTTP? Yes — JSON-RPC request-response works fine over a WebSocket, and some apps route everything through one socket. The trade is that you have concentrated all your eggs in one stateful connection, and lost easy retries and load-balancing for calls that did not need push semantics. Most production stacks keep request-response on HTTP deliberately.

Do WebSocket subscriptions guarantee delivery? No. Subscriptions deliver on a best-effort basis to a connected client. Anything emitted while you were disconnected is simply not delivered, which is why gap healing on reconnect is mandatory for correctness, not an optimization. If you need guaranteed processing of every event, your source of truth is the chain itself, re-read over request-response.

Why does my dapp stop updating until I refresh the page? Almost always a dead WebSocket nobody noticed. Browsers, laptops sleeping, and proxies all kill idle sockets. Add a heartbeat — if no newHeads arrives for, say, 30 seconds, tear down and reconnect — and the symptom disappears.

Are server-sent events or webhooks an alternative to WebSocket RPC? Some providers offer webhook products that push chain events to your backend over HTTP, which outsources the connection-babysitting problem. They are a good fit for server-side reactions with relaxed latency needs. For sub-second reactions or client-side updates, native subscriptions remain the tool.

Should my backend and frontend use the same transport strategy? Usually not. A backend service holds one long-lived, monitored WebSocket in an environment you control, which is the easy case. A frontend opens a socket per browser tab, on networks you do not control, on devices that sleep — the hard case. Many teams therefore subscribe once on the backend and fan events out to frontends through their own channel, so the tricky reconnection logic lives in exactly one place.

Does WebSocket cost more with providers compared to HTTP RPC? It is metered differently, not automatically more: per message, per subscription, and per concurrent connection. Replacing chatty polling with a quiet subscription usually reduces billed usage; adding a pending-transaction firehose usually increases it. Price out message volume for your specific subscription types before committing.

Is there a difference in latency between HTTP and WebSocket for single calls? Minimal. For a single request, the overhead of upgrading to WebSocket and maintaining the socket often outweighs the benefit. HTTP connection reuse and keep-alive achieve similar performance. The latency advantage of WebSocket is only significant when you need event-driven push; for request-response, HTTP is fine.

Sources

  1. [Geth documentation

    Used for: real-time events and eth_subscribe over WebSocket/IPC](https://geth.ethereum.org/docs/interacting-with-geth/rpc/pubsub) (accessed September 2026)

  2. [Ethereum.org

    Used for: JSON-RPC API documentation](https://ethereum.org/en/developers/docs/apis/json-rpc/) (accessed September 2026)

  3. [RFC 6455

    Used for: The WebSocket Protocol](https://datatracker.ietf.org/doc/html/rfc6455) (December 2011)