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-request

A 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.all or 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.