How to Cut RPC Costs with Response Caching: A 2026 Guide

How to Cut RPC Costs with Response Caching: A 2026 Guide

Most teams that overspend on RPC are not making expensive calls. They are making cheap calls too many times. Before you negotiate a bigger plan or shard traffic across a second provider, measure how much of your read volume is the same question asked repeatedly within a window where the answer could not have changed. In most application workloads that share is large, and it is the cheapest thing you will ever fix.

The reason it goes unnoticed is that no single call looks wasteful. A price widget polling eth_blockNumber every two seconds, twelve pods each holding their own connection pool, a webhook handler re-fetching the same block for every event it processes. Each of those is defensible in isolation. Added together they are the bill.

  • Cache the reads whose answers are immutable once written: historical blocks, mined receipts, confirmed transactions.
  • Deduplicate concurrent identical requests so a traffic spike costs you one upstream call instead of two hundred.
  • Never cache anything you are about to make an irreversible decision on. Finality is a correctness boundary, not a performance one.
  • Measure hit rate per method, not in aggregate. An overall number hides which cache is actually earning its place.

Where RPC Spend Actually Goes

Providers meter differently, whether in compute units, credits or flat request counts, but the shape of the bill is consistent across them. A small number of method families generate most of the volume, and they are almost never the complex ones.

Chain-tip polling is usually first. Any component that needs to know “has anything happened yet” tends to ask on a timer, and the timer is usually set by whoever wrote it first. Block and receipt fetching is second, and it is frequently duplicated: an indexer reads a block, then a separate reconciliation job reads the same block an hour later, then a support tool reads it again when someone files a ticket.

Balance and state reads come third, and these are the ones where caching gets genuinely difficult, because the correct cache lifetime depends on what you are going to do with the answer. Displaying a balance in a dashboard tolerates staleness. Deciding whether to release goods does not.

The practical first step is not architectural. Log the method name and the caller for every outbound RPC call for one day, then group by method. Teams are routinely surprised by which internal component is responsible for the majority of their traffic, and the answer is frequently a health check or a piece of monitoring nobody has looked at since it was written.

The Reads That Are Safe to Cache Forever

Blockchain data has an unusual property that makes caching easier than in most systems: a large portion of it is immutable once it is deep enough in the chain. A block at a given height, past the reorg window for that chain, will never change. Neither will its transactions or their receipts. These can be cached indefinitely, and the only reason not to is storage cost.

The qualifier matters. “Past the reorg window” is chain-specific and is not a number you should guess at. Treat a recently produced block as mutable until your application’s own confirmation threshold has passed, and only then promote it into the permanent cache. If you already have a confirmation policy for payments or settlement, reuse that number rather than inventing a second one. Two different definitions of “confirmed” inside one system is a bug waiting to be filed.

The reads that are never safe to cache are the ones describing current or pending state: mempool contents, pending nonces, unconfirmed balances, and anything you are polling precisely because you expect it to change. Caching these does not save money, it produces wrong answers slowly.

Layer One: Cache Inside Your Application

The cheapest cache is the one closest to the caller, and it requires no infrastructure. Key on the full request, method plus normalized parameters, rather than on the method alone, and normalize before hashing. eth_getBlockByNumber with "0x13a4f20" and the same call with an unpadded or differently cased hex string are the same question, and if your key does not know that, your hit rate silently halves.

Set the lifetime from the data, not from a global default. Immutable historical data gets no expiry. Chain-tip data gets a lifetime shorter than the chain’s block time, which for a fast L2 may mean the cache is only absorbing burst traffic rather than saving calls over time. Still worth having, but do not expect the same hit rate you would see on Bitcoin.

One caveat that catches teams building agent and automation tooling: an in-process cache inside a short-lived worker is close to useless, because the process exits before the cache is warm. If your workload is serverless or spawns per-task processes, the cache has to live outside the process or it will not exist in any meaningful sense. This is a common failure in the kind of AI-assisted Web3 development workflows where each tool invocation is its own short-lived context.

Layer Two: Collapse Concurrent Duplicates

Caching helps when the same request arrives twice in sequence. It does nothing when the same request arrives fifty times simultaneously, because none of them find a populated cache. They all miss, they all go upstream, and they all write the same value back. This is the pattern that turns a modest traffic spike into a rate-limit incident.

The fix is request coalescing, sometimes called single-flight: when a request for a key is already in progress, later callers for that key wait on the in-flight result rather than issuing their own call. The implementation is a map from cache key to a pending promise or future, and it is perhaps thirty lines of code in most languages.

The effect is disproportionate to the effort. Under steady load coalescing does very little. Under exactly the conditions that break systems — a popular block being requested by every worker at once, a retry storm after a brief outage, a cold start where every instance warms simultaneously — it is the difference between one upstream call and a self-inflicted denial of service. It also composes cleanly with the retry logic covered in handling RPC rate limits and 429 errors, since coalescing reduces the number of clients that need to back off in the first place.

Layer Three: A Shared Cache Above Your Endpoints

Application-level caches are per-deployment. If you run several services, several regions, or several environments against the same chains, each of them warms its own cache independently and pays for the privilege. A cache that sits in front of your upstream endpoints, shared across everything that calls through it, removes that duplication.

This is the layer where a gateway is useful, and it is worth being precise about what a gateway actually is, because the category name is used loosely. A node provider such as Alchemy or QuickNode operates the nodes and sells you access to them. A gateway sits between your application and whichever endpoints you already have, handling authentication, routing, retries and caching in one place. It does not replace your provider. It reduces how often you reach it.

Aurpay’s RPC Gateway is one implementation of that layer, free to use and open source under Apache-2.0. Its shared cache, called Accelerator, is checked before any endpoint is contacted; a hit returns immediately with no upstream call at all, and a miss continues through your configured endpoints as normal. The public dashboard currently reports a cache acceleration rate of 15.69% across roughly 910,000 calls. That is a useful reference point for what a shared cache absorbs on mixed real-world traffic, and a reminder that the figure is a fraction rather than a majority.

The design decision worth copying, whether or not you use that particular gateway, is how narrowly the cache is scoped. Accelerator answers only an explicit list of method and parameter shapes: eth_blockNumber, eth_getBlockByNumber at an explicit hex height with the full-transactions flag set to false, debug_traceBlockByNumber with the call tracer, the Solana equivalents, and the Bitcoin and Litecoin block reads. Block tags such as latest, safe, finalized and pending are deliberately not accepted, because a tag does not identify a specific piece of data and therefore cannot be cached correctly. A narrow cache that is always right beats a broad cache you have to reason about.

What a Cache Layer Should Not Promise

Two limitations are worth stating plainly, because vendors tend to leave them out and they determine whether the layer is safe for your use case.

A cache hit is not guaranteed. Not every height, slot or hash will be present, entries expire, and a request that succeeded yesterday can miss today. Any code path that treats a cache as a source of truth rather than an optimization will eventually fail in a way that is hard to reproduce. On the Aurpay gateway a miss with no endpoint configured returns error code -32004 No available Endpoint, which is a legitimate response to a well-formed request, not a fault.

Cached results can also lag. That is acceptable for a dashboard and unacceptable for a settlement decision. If the answer determines whether goods ship or funds move, read it from an endpoint and apply your own confirmation rules. This is the same boundary that separates reading chain state from accepting payments: related problems, but different systems with different correctness requirements, as covered in our crypto payment API guide.

Measuring Whether It Worked

Track hit rate per method rather than as a single number. An aggregate figure tells you almost nothing: a 40% overall hit rate could be a healthy cache on block reads, or it could be one extremely chatty poller being served from cache while everything expensive still goes upstream.

Watch upstream call volume rather than request volume. Those are the same number before you add a cache and should diverge sharply afterwards. If they do not, your keys are probably not normalizing correctly, or your lifetimes are shorter than your call interval. A cache with a five-second lifetime serving a poller on a six-second timer never hits.

Finally, check for staleness bugs deliberately rather than waiting for them. Write a test that asserts your cache is bypassed on the specific paths where freshness is required. Caching failures are silent by nature: nothing errors, the numbers are simply wrong, and by the time anyone notices the cause is several weeks behind you.

Frequently Asked Questions

Does caching RPC responses risk serving incorrect data?

Only if you cache the wrong things. Immutable data past your confirmation threshold, such as historical blocks, mined transactions and receipts, carries no correctness risk. Pending state, mempool contents and unconfirmed balances should never be cached. The failure mode is not caching itself, it is applying one lifetime policy to both categories.

How much can caching realistically reduce RPC costs?

It depends entirely on workload shape, so treat any single percentage with suspicion. Read-heavy applications with repetitive access patterns see the largest reduction; workloads dominated by unique state queries or writes see very little. As a reference point, the Aurpay gateway’s public dashboard reports a 15.69% cache acceleration rate across mixed traffic. Measure your own duplicate rate before assuming a number.

Is a gateway cache better than caching in my application?

They solve different problems and work well together. An application cache is faster and has full context about what the data is for. A gateway cache is shared across every service and environment that calls through it, so it does not need warming per deployment. Most teams that care about this run both.

Can I cache eth_getBlockByNumber with the latest tag?

You should not, and well-designed cache layers will refuse to. A block tag identifies a moving position rather than a specific block, so a cached response keyed on the tag returns whatever was current when it was stored. Resolve the tag to an explicit height first, then cache against that height.

Does a shared cache mean my requests are visible to other users?

A shared cache stores chain data, which is public by definition. The same block returned to you is the same block returned to anyone. What must stay scoped to your account is authentication, rate limiting and usage accounting, all of which apply per request on both hits and misses on the Aurpay gateway. Verify that separation in any provider you evaluate.

Start With Measurement, Not Migration

The order matters more than the tooling. Log your calls and find the duplicates, add coalescing where concurrency is causing simultaneous misses, cache the immutable reads with lifetimes derived from your own confirmation policy, and only then decide whether a shared layer above your endpoints is worth adding. Teams that reverse this order tend to add infrastructure and keep the bill.

If you do want the shared layer, the Aurpay RPC Gateway is free, open source under Apache-2.0, and covers 20 chain and network combinations across Ethereum, Polygon, BNB Smart Chain, Arbitrum, Optimism, Base, Solana, Bitcoin, Litecoin and TRON behind a single API key. It sits in front of the endpoints you already use rather than replacing them, so adopting it does not require moving your providers.

Aurpaytech

The Aurpay team

Aurpay is a non-custodial crypto payment gateway helping merchants accept Bitcoin, Lightning, and stablecoin payments without giving up custody of their funds.