Most Ethereum performance problems get blamed on the wrong thing. Developers tune their contract calls, optimize gas estimates, and cache responses, but skip right past a decision they made in five seconds at the start of the project: which transport to use for JSON-RPC.
That choice matters more than most people realize, and it compounds badly at scale.
What You're Actually Choosing Between
Ethereum execution clients expose JSON-RPC over three transports: HTTP, WebSocket, and Unix Domain Sockets. They look interchangeable until your app starts behaving in ways that are hard to debug.
HTTP is the default choice for most developers because it's simple. Each call is a stateless request-response cycle. That's fine for one-off reads, balance checks, or anything where you control the call frequency. The problem is overhead. Every request opens a connection, negotiates, sends, receives, and closes. Under load, that overhead adds up fast.
WebSocket keeps the connection open. This matters for subscriptions: eth_subscribe lets you stream new blocks, pending transactions, or log events without polling. If your app needs to react to chain state in real time, HTTP cannot do this cleanly. You can poll every second and still miss events or hammer rate limits. WebSocket is the right tool for persistent, event-driven workloads.
Unix Domain Sockets are the least discussed option and the most underused. If your application runs on the same machine as the node, IPC over a Unix socket removes the TCP stack entirely. Latency drops noticeably. For local tooling, bots, or tightly coupled services co-located with a node, this is often the fastest path available.
Here's a basic example of subscribing to new block headers over WebSocket using ethers.js:
import { WebSocketProvider } from "ethers";
const provider = new WebSocketProvider("wss://your-node-endpoint");
provider.on("block", (blockNumber) => {
console.log("New block:", blockNumber);
});
Swap that for HTTP and you're now polling. Polling introduces artificial latency and burns through request quotas.
Post-Merge Node Architecture Changes the Picture
After The Merge, running a full Ethereum node means running two clients in parallel: an execution layer client (like Geth or Nethermind) and a consensus layer client (like Lighthouse or Prysm). They communicate internally over the Engine API, which is a separate authenticated JSON-RPC interface.
This matters for RPC configuration because the endpoints are no longer coming from a single process. Your execution client handles the standard JSON-RPC calls your app makes. The consensus client handles validator duties and beacon chain data. They are separate ports, separate authentication, and sometimes separate machines in production setups.
If you're configuring a node from scratch or debugging a connection issue, make sure you're hitting the execution layer RPC port (typically 8545 for HTTP, 8546 for WebSocket) and not the consensus client's REST API or the Engine API port (8551), which requires JWT authentication and is not meant for application traffic.
Public Nodes Are Fine Until They're Not
Public RPC endpoints from providers are genuinely useful for development and testing. They save setup time and you can get moving immediately. The tradeoff is that you're sharing infrastructure with everyone else hitting that endpoint.
Rate limits on public nodes are not generous by design. A simple application polling for events or making frequent state reads will hit those limits quickly. When you do, requests start failing or getting queued, and your app's behavior becomes non-deterministic in ways that are annoying to reproduce.
For production applications, a dedicated node gives you a few things that are hard to work around otherwise:
- Rate limit control: You set the ceiling, not a shared provider policy
- Consistent latency: No noisy neighbors competing for the same connection pool
- Transport flexibility: You can actually use WebSocket subscriptions reliably without worrying about the provider dropping persistent connections under load
- Archive access: Querying historical state requires archive nodes, which are often restricted or unavailable on free public tiers
The point is not that public nodes are bad. It's that they're optimized for convenience, not for applications that depend on RPC behavior being predictable.
The Practical Takeaway
Match the transport to the workload. HTTP for stateless, low-frequency calls. WebSocket for anything event-driven or subscription-based. Unix sockets if you're running close to the node and need every millisecond.
Get clear on the post-Merge two-client architecture before you configure anything in production, because the endpoint you're supposed to call and the one you're accidentally calling are not always obvious.
And if your app's reliability depends on RPC calls behaving consistently, dedicated infrastructure is not a luxury. It's just the cost of building something that works.
Top comments (0)