How to Reduce RPC Latency
Slow RPC kills user experience. A wallet that takes two seconds to refresh balance feels broken; a trading bot that polls every 500ms but pays 200ms round-trip is leaving money on the table. Here are the techniques that actually move the needle, in order of impact.
1. Batch your requests (biggest single win)
JSON-RPC supports batching natively. Instead of sending 50 separate HTTP requests for 50 contract reads, pack them into one batch request. You pay the network round-trip once instead of 50 times.
// Bad: 50 round-trips
for (const addr of addresses) {
await provider.getBalance(addr);
}
// Good: 1 round-trip (most providers / libraries support this)
const balances = await Promise.all(
addresses.map(a => provider.getBalance(a))
);ethers.js and viem both auto-batch Promise.all sequences over the same provider into a single JSON-RPC batch when you use the default batched transport. For raw curl/fetch, send an array instead of a single object as the request body.
2. Use WebSocket for subscriptions, not polling
If you’re polling eth_blockNumberin a loop to detect new blocks, switch to a WebSocket subscription. Latency drops from “next poll interval” (typically 1-12 seconds) to single-digit milliseconds.
See our WebSocket guide for the full setup.
3. Reuse HTTP connections (keep-alive)
TLS handshake is expensive — typically 50-150ms per connection. Your HTTP client should reuse connections via keep-alive. Most modern libraries do this automatically, but verify:
// Node.js — use a single agent across requests
import { Agent } from "https";
const agent = new Agent({ keepAlive: true, maxSockets: 16 });
// Pass agent to your HTTP client or fetch wrapper
fetch(url, { agent });
// ethers.js — passes through native fetch / undici; keep-alive enabled by default
// Just make sure you reuse ONE provider instance, don't create per-requestA single shared provider instance can drop p50 latency by 80ms+ vs reinstantiating per call.
4. Co-locate your client with the RPC server
Speed of light isn’t optional. A request from San Francisco to a Frankfurt RPC node has a hard floor of ~150ms round-trip just for the photons. If your application is in AWS us-east-1 and your RPC is in EU, expect 80-100ms round-trip before any RPC processing.
SwiftNodes routes requests through nearby edge nodes based on your client IP, so a US client hits US infrastructure and an Asian client hits Asian infrastructure. If you’re self-hosting nodes, deploy them in the same region as your application servers.
5. Cache reads that don’t change
chainId, contract ABIs, token decimals, deployment block numbers — these don’t change. Cache them in your application memory or a key-value store. There’s no reason to send the same eth_chainId call 1000 times per minute.
6. Use multicall for many small reads
On EVM chains, the deployed Multicall3 contract (0xcA11bde05977b3631167028862bE2a173976CA11) lets you aggregate many contract reads into a single eth_call. Combine this with request batching for compound speedups.
// viem has built-in multicall support
const results = await client.multicall({
contracts: [
{ address: token1, abi: erc20Abi, functionName: "balanceOf", args: [user] },
{ address: token2, abi: erc20Abi, functionName: "balanceOf", args: [user] },
{ address: token3, abi: erc20Abi, functionName: "balanceOf", args: [user] },
// ... up to ~100 reads per call
],
});7. Failover, but don’t race
Multi-provider failover sounds great, but if you implement it as “send to both and use whoever responds first,” you double your costs and confuse rate-limit accounting. Better: pick a primary provider, set a 1-2s timeout, and fall back to a secondary only on timeout/error.
SwiftNodes already does internal failover across multiple upstream nodes per chain, so you usually don’t need a client-side failover layer at all.
8. Pre-warm your provider
First request to a new endpoint pays DNS resolution + TLS handshake + connection negotiation — often 200-400ms of one-time cost. If you know you’ll need RPC soon (e.g. user just opened your app), kick off a cheap warm-up call (eth_chainId) immediately so the connection is already established when the real request comes.
9. Pick the right method for the job
eth_getLogs over a wide block range is slow on every provider. eth_callis fast. Where possible, design contracts and queries so you’re reading state via eth_call rather than scanning historical logs. If you need historical logs at scale, consider an indexer (Goldsky, The Graph) so you only hit raw RPC for fresh data.
Quick checklist
- Reuse a single Provider instance per process
- Enable keep-alive on your HTTP client
- Batch independent reads with
Promise.allor multicall - Use WebSocket for anything you’d otherwise poll
- Cache immutable data (chainId, decimals, ABIs)
- Pick an RPC provider in the same region as your servers
- Set a 1-2s timeout and fall back gracefully on failure
Live network latency for every chain is published on our status page — median latency is typically under 100ms across our supported chains, measured every 60 seconds.