By using this site, you agree to the Privacy Policy and Terms of Use.
Accept
World of SoftwareWorld of SoftwareWorld of Software
  • News
  • Software
  • Mobile
  • Computing
  • Gaming
  • Videos
  • More
    • Gadget
    • Web Stories
    • Trending
    • Press Release
Search
  • Privacy
  • Terms
  • Advertise
  • Contact
Copyright © All Rights Reserved. World of Software.
Reading: Optimizing RPC Performance with Batch Requests and Caching Layers | HackerNoon
Share
Sign In
Notification Show More
Font ResizerAa
World of SoftwareWorld of Software
Font ResizerAa
  • Software
  • Mobile
  • Computing
  • Gadget
  • Gaming
  • Videos
Search
  • News
  • Software
  • Mobile
  • Computing
  • Gaming
  • Videos
  • More
    • Gadget
    • Web Stories
    • Trending
    • Press Release
Have an existing account? Sign In
Follow US
  • Privacy
  • Terms
  • Advertise
  • Contact
Copyright © All Rights Reserved. World of Software.
World of Software > Computing > Optimizing RPC Performance with Batch Requests and Caching Layers | HackerNoon
Computing

Optimizing RPC Performance with Batch Requests and Caching Layers | HackerNoon

News Room
Last updated: 2025/06/17 at 8:06 PM
News Room Published 17 June 2025
Share
SHARE

Heavy-traffic dApps that query Ethereum’s blockchain numerous times within a brief span are going to see latency and node workload congestion. Every user dashboard refresh load, token balance inquiry, or contract read issues make JSON-RPC calls to an Ethereum node – and the larger the user base, the more calls are made. This can lead to slow responses, higher fees, and a poor user experience. Here, we will learn two powerful methods of avoiding these issues: batch multiple RPC calls into one request, and introduce caching layers to reuse frequent data. With the help of web3.js batch requests along with caching (in-memory and through tools such as Redis or Memcached), full-stack developers can effectively reduce latency and lighten the load on Ethereum nodes.

Why Batching and Caching Are Important to High-Traffic dApps

Batching and caching solve the inefficiencies of redundant RPC calls. Briefly, batching decreases the overhead per request by batching calls, and caching eliminates repeated requests. When both are used together, they conserve throughput and responsiveness. The result is an improved user experience (a faster UI update) and an optimized backend that can handle more traffic with less pressure.

Batching JSON-RPC calls

Ethereum’s JSON-RPC API allows for batch requests, meaning multiple requests can be sent in a single HTTP round trip. This minimizes the number of HTTP requests and network round-trip overhead required to fetch data. Fewer round trips mean less aggregate latency and less CPU time for request handling. In fact, bundling multiple calls in a single request can lower client-server overhead and response time dramatically. Researchers found that batched requests saved around 20% of total request time compared to sending requests individually. Batch requests also guarantee atomicity – all the calls in the batch are made as a unit. Batching in general makes performance better by doing more work per network call.

Caching frequent data

Caching is the technique of putting frequently accessed data in fast storage (memory) so that subsequent requests can get it faster without having to ping the remote node repeatedly. By repeating cached queries back to you, you reduce RPC calls made to your Ethereum node and respond with results more rapidly out of memory. This is especially applicable to heavy-traffic dApps where many users are retrieving the same data (e.g. token prices, last block data) or where frontends are polling for new data. Instead of every user building a new blockchain query, caching can be the door through which most RPC requests happen only once in a window of caching and subsequent calls are served by the cached result. As an example, without cache, 10,000 users requesting an on-chain price would equate to 10,000 node calls. With cache, if the data is cached for say 60 seconds, the 10,000 users may be answered to with one RPC call (the first request populating the cache) and 9,999 fast cache hits. This takes huge amounts of workload and latency off nodes.

Batching JSON-RPC calls with Web3.js

Web3.js has a built-in BatchRequest feature that makes it straightforward to batch multiple JSON-RPC calls into one HTTP request. This is achieved behind the scenes with the Ethereum node support for sending an array of requests and getting an array of responses in one go. Instead of making, for example, five separate HTTP requests to fetch five distinct bits of information, you can make one request containing all five queries. The node will run them (typically in parallel) and return all the responses in a single transmission, sparing a lot of network overhead.

Using web3.eth.BatchRequest

To build a batch request in web3.js (v1.x and above), you start by initializing a new web3.BatchRequest() instance and then append single calls to it. You add every call using the method’s .request() function along with its arguments and a callback to handle the response. Finally, you execute the batch with batch.execute(). All requests in the batch are sent at once.

For example, suppose we need to retrieve the Ether balance of some addresses en masse. We’d typically call web3.eth.getBalance(address) one by one in a loop, which does separate RPC calls. Instead, we can batch them:

const Web3 = require('web3');
const web3 = new Web3('https://your.ethereum.node');  // Your Ethereum node RPC URL

const addresses = [
  "0x1f9090aaE28b8a3dCeaDf281B0F12828e676c326",
  "0x2bB42C655DdDe64EE508a0Bf29f9D1c6150Bee5F",
  // ... more addresses
];

async function getBalancesBatch() {
  const batch = new web3.BatchRequest();
  addresses.forEach((addr) => {
    batch.add(web3.eth.getBalance.request(addr, 'latest', (err, balanceWei) => {
      if (err) {
        console.error(`Error fetching balance for ${addr}:`, err);
      } else {
        const balanceEth = web3.utils.fromWei(balanceWei, 'ether');
        console.log(`${addr} balance: ${balanceEth} ETH`);
      }
    }));
  });
  // Send all requests in one HTTP round trip
  batch.execute();
}

getBalancesBatch();

In the above example, we add a number of eth_getBalance requests to the batch, then invoke them all together. Every address balance will be printed when the batch response returns. This batching is much better in this instance than creating a loop of individual requests, because it prevents the redundant cost of numerous HTTP requests. All the balance requests are fulfilled in one network call.

How it works? When batch.execute() is called, web3.js sends a single JSON payload containing an array of RPC request objects (each with its own method, params, and id). The Ethereum node processes each and replies with an array of results. Web3.js then invokes the respective callbacks with those results. A batch’s network latency is roughly equivalent to a single request, so batching (e.g.) 10 calls means you’ve essentially reduced what would’ve been 10 round trips to 1. All those calls being processed in one round trip significantly minimizes client-server overhead and improve overall response time.

Example Use Cases for Batch Requests

Batching comes in handy when your dApp needs to fetch a number of pieces of data simultaneously if possible. A few common scenarios include:

Fetching multiple token balances

Consider a wallet dApp displaying a user’s balances of different ERC-20 tokens. Instead of calling each token contract’s balanceOf separately (which would be many eth_call RPC calls), you can batch them all. For example, you can batch balanceOf(user, TokenA), balanceOf(user, TokenB), balanceOf(user, TokenC), and so on, in a single request. The node will send back all the token balances in a single response. This gives a quicker, consolidated response and minimizes load.

On-chain multicall contracts can also achieve a similar goal, but off-chain JSON-RPC batching is simpler to implement and was found to slightly outperform on-chain multicalls in some cases.

Fetching several transaction or block information

If your application needs data from several blocks or transactions (e.g., retrieving a list of blocks in the past, or data on numerous transaction hashes), batch is what you use. For example, to get data for block N, N+1, and N+2, you can put three eth_getBlockByNumber calls into a BatchRequest instead of making them individually. You can batch multiple calls to eth_getTransactionReceipt if you’re calling a list of transaction hashes. The node will call them sequentially and respond with an array of block or transaction objects in a single call.

Batching multiple contract calls at once

Any scenario where you need to call hundreds of read-only smart contract functions can benefit from batching. To give an example, a DeFi dashboard might have several contracts request data (prices, total supply, user positions) on load. Using web3.js, you can have all of those contract.methods.myMethod().call requests prepared and place them in a batch. The backend of the dApp then makes a single request to the node and receives all the data required at once, improving load time.

Best Practice. Keep batch sizes reasonable and be mindful of provider limits. Some providers have a limit on the number of calls per batch (e.g., Alchemy has 1,000 calls per batch over HTTP). Also, extremely large batches are slower to handle or can be larger than response size limits. It’s generally effective to batch a small group of calls that only make sense as a batch (e.g., the data for a single page or single user action). Also remember that if one call in the batch fails, it won’t prevent others from succeeding, but you’ll need to process error responses on a per call basis within the batched response. As a general rule, batching JSON-RPC requests is an easy solution to reduce latency per page load or per-user event via minimal round trips.

Caching Ethereum RPC Responses in Node.js

While batching reduces per-request cost, caching resolves the problem of redundant requests. There are many dApps that are asking some data frequently or many users asking the same data (e.g., current ETH price, or total supply of a trending token, or a user balance being asked for change). Keeping these answers in an optimized store can largely eliminate redundant calls and improve response.

How it helps? Once we pull an item of data from the blockchain, we cache it (in memory or some other cache storage). Subsequent requests for the same information can be served from the cache in microseconds without making a network round trip to the node. Caching copies frequently accessed data to fast storage close to the application, improving performance and scalability by reducing how often you hit the backend system. caching “frequently-used API data in memory” means a local copy is available, which can be accessed much more quickly than fetching from a remote RPC endpoint every time.

There are some caching methods that you can implement in Node.js:

In-Memory Caching (LRU Cache)

For a single Node.js process or a small-scale application, in-memory caching is the simplest. Data is stored in the memory (RAM) of the Node process for quick access. It works for data that is looked up frequently but doesn’t modify frequently. You can restrict memory usage and remove the least recently used elements when the cache is full by using an LRU (Least Recently Used) cache.

Implementation. You can use an in-memory cache with widely known libraries like node-cache, memory-cache, or lru-cache. For example, using the lru-cache package:

// Install with: npm install lru-cache
const LRU = require('lru-cache');

// Create an LRU cache that holds up to 500 items and expires items after 60 seconds
const cache = new LRU({
  max: 500,               // maximum number of items
  ttl: 1000 * 60          // time-to-live in ms (here, 60s)
});

// Function to get token balance with caching
async function getTokenBalanceCached(tokenContract, userAddress) {
  const cacheKey = `${tokenContract.options.address}:${userAddress}:balance`;
  const cachedValue = cache.get(cacheKey);
  if (cachedValue) {
    console.log('Cache hit for token balance');
    return cachedValue;  // return the cached balance (assumed to be still fresh)
  }
  console.log('Cache miss, fetching from blockchain...');
  // Call the blockchain (e.g., ERC20 balanceOf)
  const balance = await tokenContract.methods.balanceOf(userAddress).call();
  cache.set(cacheKey, balance);
  return balance;
}

Here in this snippet, we check if we have a cached result for the user’s token balance before making the RPC call. If we do, we return that immediately (“From Cache”). Otherwise, we read from the blockchain and then cache the result for next time. We also set a TTL (time-to-live) so that the entry will be removed after, say, 60 seconds, so we don’t return stale data forever. This approach is similar to the example of caching API responses for 1 minute using an in-memory cache, which highlighted that frequently accessed data that doesn’t change often is a good candidate for short-term caching.

When to use? In-memory caches are fast (no network calls needed to retrieve data) and easy. However, the cached data is present just in that Node.js process – if you have several instances of your server, each one will have its own separate cache. Memory caches are also transient: when the process restarts, the cache is discarded. Despite these limitations, in-memory caching is ideal for small-scale use or for caching data that is accessed often within a brief period by the same process.

Distributed Caching using Redis or Memcached

For larger applications or multiple instances of servers, it is recommended to have a distributed cache so that the cache is shared and persisted in the environment. Tools like Redis and Memcached are in-memory data stores that operate as separate services. Your Node.js application can this cache store over the network. Redis is very popular because of its performance and feature richness, and Memcached is a lightweight cache optimized for straightforward key-value caching. Both can significantly offload database or node reads when used as a caching layer.

Implementation with Redis. To use Redis in Node.js, you can use the official redis client or community clients like ioredis. First, ensure you have a running Redis server and connect to it:

// Install with: npm install redis
const redis = require('redis');
const client = redis.createClient({ url: 'redis://localhost:6379' });
await client.connect();  // connect to Redis server

async function getLatestBlockNumberCached() {
  const cacheKey = 'latestBlockNumber';
  // Try to get the value from Redis
  const cachedVal = await client.get(cacheKey);
  if (cachedVal !== null) {
    return Number(cachedVal);  // return cached block number
  }
  // If not in cache, fetch from Ethereum node
  const latestBlock = await web3.eth.getBlockNumber();
  // Store in cache with an expiration (e.g., 5 seconds, since blocks update frequently)
  await client.set(cacheKey, latestBlock, { EX: 5 });
  return latestBlock;
}

Here we attempt to get a cached "latestBlockNumber" from Redis. If present, we return immediately. If not present, or null, we call web3.eth.getBlockNumber() to ask the node, and then cache that number in Redis with an expiration of 5 seconds (EX=5 seconds). The second invocation within a timespan of 5 seconds will be against the cache. This decreases the number of times the node must be hit for the new block height — something that could be asked frequently by a large number of clients. Redis is a shared memory for all your Node processes, so regardless of the number of instances of your application that are running, they’re all checking against the same cache. The performance benefit is huge: loading data from Redis (which resides in memory) is on the order of micro- or milliseconds, a great deal faster than an RPC that may take tens or hundreds of milliseconds and consumes node resources.

Implementation with Memcached. It works the same way using Memcached: you employ a Node memcached client (such as the memcached npm package), connect to a Memcached server, and call client.get(key) / client.set(key, value) with an expiration. The same logic of checking cache first and then defaulting to the RPC call applies. Memcached is a tad simpler (strings only, no persistence, LRU eviction by default) and can be very, very fast for cache reads. Your choice between Redis and Memcached will then boil down to your stack and requirements – both are excellent caching layers for RPC data.

Cache Invalidation and Stale Data Management

One of the biggest caching problems is cache invalidation – i.e., when to refresh or expire cached information. For blockchain information, we have to be careful to balance freshness and performance.

TTL (Time-to-Live)

Always provide an appropriate TTL to cached RPC results depending on how quickly the underlying data may change. For example, it is okay to cache the current block number for 5-10 seconds because new blocks arrive approximately every ~12 seconds on Ethereum. Token balance caching might be usage-based – if the user is not going to notice new transactions within the next minute or so, a 30-60 second cache should be fine. But for very volatile data (like a rapidly updating on-chain price), you might cache for a few seconds or not cache at all. The point is to avoid serving stale data that could confuse or mislead users. You employ short TTLs so the cache will flush anyway even though you don’t take the time to invalidate it.

Explicit Invalidation on Events

In addition to time-expiry, you can explicitly invalidate or refresh cache values whenever you learn of a pertinent on-chain event. For instance, if your backend just saw (or published) a transaction that changes a user’s token balance, you need to invalidate the cache for that balance key immediately. This will lead to the next read to fetch the new value from the node (or even cache the new anticipated value itself).

Another example: if you’re caching everything about a block, when you get a new block, you can invalidate the cached “latest block” data so the next request gets new information. By tying cache invalidation to real events (new blocks, user transactions, contract state changes), you minimize stale data.

Avoiding Stale or Incorrect Data

For critical real-time data, don’t cache at all, or employ strategies like stale-while-revalidate. Stale-while-revalidate is the approach wherein you provide the cached content right away (to not delay) but also initiate an asynchronous refresh in parallel. This manner the user gets something instantly, and if previously stale, your system will refresh the cache for the future soon enough. Where absolute real-time is required (e.g., showing the status of a newly-mined transaction), a cache would be omitted altogether for the sake of plain RPC call or WebSocket subscription.

Remember. Caching is a trade-off between performance and freshness – for many read-heavy use cases, it’s a fair trade to cache for even a few minutes or seconds that greatly improves performance, but you have to figure out what aspects of your app can tolerate small delay in data vs. which require up-to-the-second accuracy.

User-Specific Data

Steer clear of caching data unique to an individual user (e.g., their account balance or their own personal portfolio details). If sharing a cache, namespace keys by user so that there’s any chance for leaks (e.g., add the user’s address or ID to the beginning of the cache key, as we’ve done with token balance in the cacheKey). For truly sensitive information, opt to cache it only in the browser of the user (through a front-end cache or localStorage) or not at all, to eliminate even the possibility of another user accessing it. Always steer clear of caching personal user details in a way that can be sent back to other users – use per-session or per-user caches (or disable caching for those routes).

Cache Warming and Fallbacks

In high-traffic systems, it’s common to pre-populate caches (cache warming) for known expensive queries so that users never experience the cold cache latency. Additionally, consider a fallback strategy: if the Ethereum node is under heavy load or temporarily unreachable, your system might serve the last known cached data (with a flag indicating it might be outdated) rather than failing completely. This is a form of graceful degradation. Some implementations use a circuit breaker pattern where if RPC calls are failing or timing out, the service returns a cached response to keep the application running.

For example, if you cannot fetch the latest price feed due to a network issue, you return the price from a few minutes ago from cache with a warning. This ensures continuity of service – the user sees somewhat stale info instead of an error message.

Improving UX and Efficiency with Batching & Caching

By adopting batching and caching, you are able to drastically improve the user experience on the front-end as well as the back-end performance of your dApp.

Decreased Latency for Users

Batching avoids unnecessary waiting by retrieving multiple bits of information in a single request. Users are able to load their dashboards and data faster because the overhead associated with each incremental RPC call is eliminated. Instead of loading in sequence (which can waterfall into a slow experience), batched calls provide data nearly in parallel. Caching adds to the illusion of speed – repeated operations or visits can be virtually instantaneous when the data is accessed from memory. Such responsiveness retains the users focused and pleased.

Less Node Workload

From the standpoint of the node, batching and caching largely reduce the requests coming in. Rather than, for example, 100 individual JSON-RPC requests to the GetBlock node, you get 1 batch request or a handful of cache-miss requests. That results in less CPU utilization, less network I/O, and a higher number of users on the same node hardware. Streaming frequent reads to caches avoids making the node repeat work. In practice, dApps that implemented caching have seen dramatic drops in RPC traffic – thousands of user requests might boil down to a single cached query per minute. This not only improves performance but can also cut costs if you’re using a paid RPC provider (since you’re making fewer calls overall).

Scalability and Stability

Busy periods (e.g., when many users are online or when a very popular NFT drop takes place) will inundate an Ethereum node with requests. Batching ensures your app is still making good use of each request during load, and caching shields your app from spikes by alleviating read demand. The result is a more scalable system that’s less likely to hit provider rate limits or timeouts. Your dApp remains stable and responsive under traffic bursts because the underlying RPC calls are optimized and controlled.

Conclusion

Optimizing RPC performance using batch requests and caching layers is best practice for any serious dApp. These approaches are complementary: batching maintains low latency by doing more per network call, and caching prevents having to call as much by recycling results. By carefully using both, you can deliver a snappier, more efficient application. The user has an easy, smooth experience, and your Ethereum node (or RPC provider like GetBlock) gets considerably less load – a win-win for both user experience and backend health.

Sign Up For Daily Newsletter

Be keep up! Get the latest breaking news delivered straight to your inbox.
By signing up, you agree to our Terms of Use and acknowledge the data practices in our Privacy Policy. You may unsubscribe at any time.
Share This Article
Facebook Twitter Email Print
Share
What do you think?
Love0
Sad0
Happy0
Sleepy0
Angry0
Dead0
Wink0
Previous Article Trump is giving TikTok another ban extension
Next Article GOP squares off over AI ban
Leave a comment

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

Stay Connected

248.1k Like
69.1k Follow
134k Pin
54.3k Follow

Latest News

Intel Set to Lay Off Up to 20% of Its Factory Workforce
News
Tesla China sales return to growth in August despite competition · TechNode
Computing
Clinical AI startup Nabla to focus on agentic automation after raising $70M in funding – News
News
This AI image generator lets you make ANYTHING—even NSFW art—and it’s only $40 for life
News

You Might also Like

Computing

Tesla China sales return to growth in August despite competition · TechNode

1 Min Read
Computing

Ukraine, GPS, and the Problem with Smart Weapons | HackerNoon

7 Min Read
Computing

Iran Slows Internet to Prevent Cyber Attacks Amid Escalating Regional Conflict

4 Min Read
Computing

Veeam Patches CVE-2025-23121: Critical RCE Bug Rated 9.9 CVSS in Backup & Replication

2 Min Read
//

World of Software is your one-stop website for the latest tech news and updates, follow us now to get the news that matters to you.

Quick Link

  • Privacy Policy
  • Terms of use
  • Advertise
  • Contact

Topics

  • Computing
  • Software
  • Press Release
  • Trending

Sign Up for Our Newsletter

Subscribe to our newsletter to get our newest articles instantly!

World of SoftwareWorld of Software
Follow US
Copyright © All Rights Reserved. World of Software.
Welcome Back!

Sign in to your account

Lost your password?