Every Ethereum RPC call travels over a transport, and the transport you pick sets a floor on how fast that call can ever be. For a wallet showing a balance, the difference is invisible. For an arbitrage bot that reads a price and decides whether to act before the next block, it is the whole game. This post compares the three transports your node speaks, HTTP, WebSocket, and IPC, explains where their latency comes from, and gives you a benchmark you can run to see the gap on your own setup.
The three transports
HTTP. Request and response. Each call is an HTTP POST with a JSON-RPC body. Simple, stateless, and universally supported. For subscriptions it cannot push, so libraries fall back to polling.
WebSocket. A persistent, bidirectional connection. You pay the handshake once, then send many requests over the open socket and, crucially, receive server-pushed events. This is how you subscribe to new blocks or pending transactions with eth_subscribe.
IPC (Unix domain socket). The same JSON-RPC, but over a local socket file such as /tmp/sockets/rpc_proxy.sock instead of a network port. There is no IP stack, no TLS, no remote host. It only works when your code and the node are on the same machine.
Where the latency comes from
A remote RPC call’s time is dominated by things that have nothing to do with Ethereum:
- Network round-trip. Even within the same datacenter, a packet there-and-back costs real time. Across regions it costs far more.
- TLS. HTTPS and secure WebSocket add handshake and per-message encryption overhead.
- Connection setup. HTTP may reopen connections; a cold TLS handshake is several round-trips before the first byte of your actual request.
WebSocket removes the repeated setup by keeping the connection open, which is why it beats HTTP for chatty workloads. But it is still a network socket: the round-trip and TLS costs remain. IPC removes the network entirely. A Unix domain socket is kernel-local memory copying between two processes, so the floor drops from milliseconds to microseconds.
The rough picture for a single eth_blockNumber as of June 2026. These are illustrative ranges, not promises: hardware, region, TLS, client load, provider routing, and library caching can all move the numbers. We have since measured the real distributions across nine public endpoints and seven local transports, with percentiles and methodology, in the 2026 Ethereum RPC latency benchmark; prefer those numbers to this table.
| Transport | Typical round-trip | Server push | Same machine required |
|---|---|---|---|
| HTTP (remote) | 50-500 ms | no (polling) | no |
| WebSocket (remote) | 50-200 ms | yes | no |
| WebSocket (localhost) | 1-5 ms | yes | yes |
| IPC (Unix socket) | sub-millisecond | yes | yes |
Benchmark it yourself
Numbers from a blog post are no substitute for measuring your own path. This script times many eth_blockNumber round-trips and reports the distribution, which matters more than an average because tail latency (p99) is what bites a bot.
npm install ethers
import { ethers } from 'ethers';
const RPC_URL = process.env.RPC_URL || 'ws://eth:8545';
const provider = RPC_URL.startsWith('ws')
? new ethers.WebSocketProvider(RPC_URL)
: new ethers.JsonRpcProvider(RPC_URL, undefined, { cacheTimeout: -1 });
function percentile(sorted, p) {
const i = Math.min(sorted.length - 1, Math.floor((p / 100) * sorted.length));
return sorted[i];
}
async function main() {
const SAMPLES = 200;
// Warm up so we measure steady-state latency, not connection setup.
await provider.getBlockNumber();
const samples = [];
for (let i = 0; i < SAMPLES; i++) {
const start = process.hrtime.bigint();
await provider.getBlockNumber();
const end = process.hrtime.bigint();
samples.push(Number(end - start) / 1e6); // ms
}
samples.sort((a, b) => a - b);
console.log(`min ${samples[0].toFixed(2)}ms ` +
`p50 ${percentile(samples, 50).toFixed(2)}ms ` +
`p99 ${percentile(samples, 99).toFixed(2)}ms`);
}
main().catch(console.error);
Run it against a remote HTTP endpoint, then a WebSocket one, then a local node, and watch the floor drop. The sample disables ethers’ JSON-RPC cache for HTTP so repeated eth_blockNumber calls measure actual round-trips rather than local cached values. A runnable version with min/avg/p50/p99/max output is in the companion project at sample-projects/rpc-latency-benchmark, and a multi-endpoint version that interleaves providers round-robin is at sample-projects/rpc-latency-matrix.
When each transport is the right call
- HTTP: stateless reads, serverless functions, anything where simplicity beats microseconds. The default for a reason.
- WebSocket: anything that needs server push, subscribing to new blocks, logs, or pending transactions, and chatty request patterns where keeping the connection open pays off.
- IPC: latency-critical code that can run on the same machine as the node. MEV searchers, arbitrage bots, high-frequency indexers. If you are racing other people to act on a block, this is the transport you want.
The catch with IPC
IPC’s advantage is also its constraint: it requires your process and the node to share a machine. With a hosted provider that is impossible by definition; their node lives in their datacenter, and the best you can do is localhost WebSocket against a node you run yourself, which means taking on sync, storage, and operations.
This is exactly the gap BLAZED.sh closes. Your container or script runs on the same server as a fully synced node, so the call never crosses the public internet.
Note: IPC is the maximum-performance local transport in general and a future BLAZED.sh direction. Today, BLAZED.sh exposes a local WebSocket endpoint for product workloads; it still skips the public internet and keeps the node call on the host.
Today, you connect over the node’s local WebSocket:
// Co-located with the node, so the call never leaves the machine.
const provider = new ethers.WebSocketProvider('ws://eth:8545');
For self-hosted setups with IPC enabled, or for a future BLAZED.sh IPC option, switching is a one-line change that can take the floor from local-WebSocket milliseconds toward sub-millisecond IPC:
// Coming soon: the local IPC socket removes even the loopback network stack.
const provider = new ethers.IpcSocketProvider('/tmp/sockets/rpc_proxy.sock');
Either way the win is co-location: a public internet round-trip becomes a local one, on every call your bot makes. If your eth_getLogs backfills are also slow, the same co-location gives you a predictable local path and a documented BLAZED.sh block window today, and if you are weighing providers more broadly, see our Infura alternative comparison.
Conclusion
HTTP is the simple default, WebSocket adds persistence and server push, and IPC removes the network floor entirely when it is available on the same machine. The fastest transport is the one with the least between your code and the node. Benchmark your own path first; if the tail latency is hurting, co-location is the lever that actually moves it, with local WebSocket available on BLAZED.sh today and IPC remaining the maximum-performance local target for self-hosted and future deployments.