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:
| 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 provider = process.env.RPC_URL.startsWith('ws')
? new ethers.WebSocketProvider(process.env.RPC_URL)
: new ethers.JsonRpcProvider(process.env.RPC_URL);
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. A runnable version with min/avg/p50/p99/max output is in the companion project at sample-projects/rpc-latency-benchmark.
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: Direct IPC socket access is temporarily disabled on BLAZED.sh and will be activated again soon. In the meantime you reach the co-located node over a local WebSocket, which still skips the public internet entirely and keeps latency in the low single-digit milliseconds.
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');
When IPC access returns, switching is a one-line change that takes latency from low-single-digit milliseconds down to sub-millisecond:
// 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 50 to 500 ms public round-trip becomes a local one, on every call your bot makes. If your eth_getLogs backfills are also slow, the same co-location lifts the block range limit, 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. The fastest transport is the one with the least between your code and the node, and the only way to reach sub-millisecond IPC is to put your code on the node itself. Benchmark your own path first; if the tail latency is hurting, co-location is the lever that actually moves it.