Caching Solana RPC Responses Safely

What is safe to cache, what is never safe, and why a cached blockhash is the one that turns a saved request into a wasted transaction.

BoltTx Team··8 min read
solanacachingrpcrate-limitsblockhashtrading-bot

Caching RPC responses is the cheapest way to stay inside a rate limit. It is also the cheapest way to send transactions that were never going to land.

★The difference is entirely about which call you cached.★

Cache by How Fast the Answer Changes

Call Changes Safe to cache
getMinimumBalanceForRentExemption ★almost never★ ★yes, indefinitely★
Token mint decimals ★never after creation★ ★yes, indefinitely★
PDA derivations ★never★ ★yes, indefinitely★
getLatestBlockhash every slot ★no — refresh in background★
Account data every slot briefly, if at all
getBalance every slot ★no for decisions★
getSignatureStatuses ★that is the point★ ★never★

The top three are free wins. ★Rent exemption for a given size is a protocol constant in practice, and a mint's decimals cannot change after creation★ — caching those for the lifetime of the process is correct.

const rentCache = new Map<number, number>();

async function rentExempt(space: number) {
  if (!rentCache.has(space)) {
    rentCache.set(space, await connection.getMinimumBalanceForRentExemption(space));
  }
  return rentCache.get(space)!;
}

The Blockhash Is Not a Cache

This is the distinction that matters most, and the word "cache" is what causes the confusion.

★You should absolutely avoid fetching a blockhash in the hot path. You should absolutely not serve a stale one.★ Those sound contradictory and are not — the answer is a background refresher, not a TTL cache.

let current = await connection.getLatestBlockhash("confirmed");

setInterval(async () => {
  try {
    current = await connection.getLatestBlockhash("confirmed");
  } catch { /* keep the previous value; it is still within its window */ }
}, 5_000);                          // ★refresh well inside the ~150-block window★

function getBlockhash() {
  return current;                   // ★always fresh, never a network call★
}

★A TTL cache expires and then serves nothing, or worse, serves a value past its validity window.★ A background refresher always has a usable value and always has one that is recent. The difference shows up as Blockhash not found on transactions that were built from an expired cache entry.

Always keep lastValidBlockHeight alongside the hash. It is what tells your retry loop when to stop, and a cached blockhash without it is a transaction you cannot correctly give up on.

If your caching is right and transactions still land late, a free BoltTx key is one line to test the submission path.

minContextSlot Prevents Going Backwards

Providers run pools of nodes, and they are not perfectly in sync. Two consecutive requests can hit nodes at different slots — so a balance can appear to decrease and then increase with no on-chain change.

const { context, value } = await connection.getAccountInfoAndContext(addr);
const seenSlot = context.slot;

// ★Later request: refuse anything older.★
const next = await connection.getAccountInfo(addr, {
  minContextSlot: seenSlot,
});

★minContextSlot makes the node error rather than serve you a stale view.★ For a bot tracking state incrementally, going backwards is worse than an error — an error you handle, while a silent regression corrupts whatever you derived from it.

Cache Keys That Break Silently

A caching bug worth knowing about because it is invisible until it is not:

// ★Broken: commitment is not in the key.★
// const key = `account:${address}`;

// ★Correct.★
const key = `account:${address}:${commitment}`;

Read the same account at processed and at finalized and you get different values. ★A cache keyed only on the address serves whichever arrived first, so the same code path returns different answers depending on what ran before it.★

The same applies to dataSlice, filters, and encoding. If a parameter changes the response, it belongs in the key.

What Never to Cache

Signature statuses. You are polling precisely because the answer is expected to change. A cached status is a confirmation loop that never terminates.

Balances used for decisions. Cache for display if you like. ★A trade sized against a cached balance is sized against the past, and the on-chain check will reject it.★

Simulation results. simulateTransaction runs against current state. A cached result describes a slot that has passed.

Anything you are about to spend. The pattern to internalise: cache what you read to decide, never cache what you check to commit. The commit-time check belongs on chain, inside the instruction, where it cannot be stale.

Negative Results Need Their Own Rule

Caching "this account does not exist" is where new-token bots break:

// ★Dangerous for anything new.★
if (cache.has(`missing:${ata}`)) return null;

★An account that does not exist now may exist in the next slot.★ For a token account that is the normal course of events, and a bot that cached the absence will keep believing it is absent long after it was created.

Cache negatives briefly or not at all, and never for addresses that are expected to appear.

What Landing Looks Like

Real transactions through our delivery nodes: median confirmation 336ms — under one slot.

★Caching removes round trips before you send; it does nothing after.★ A transaction built from perfectly cached inputs still competes for inclusion exactly like any other.

Where BoltTx Fits

We handle submission, not reads. Whatever caching you use for account data and derived values stays as it is.

Submissions route through our own delivery nodes in four regions with stake-weighted routing and no public mempool exposure, so a transaction is not observable in transit before it lands. Because the endpoints are independent, read traffic and cache misses cannot exhaust the path your transactions go out on.

You sign locally. We never hold funds, never sign, and never modify transaction contents. The tip travels inside the transaction, paid on chain from your own wallet, and reverts with the transaction if it fails, because that is how Solana handles atomic transactions. You pay only on transactions that reach the chain.

Get a free API key. No monthly fee:

const sender = new Connection("https://la.bolttx.io/?api-key=YOUR_KEY");

FAQ

What Solana RPC responses are safe to cache? Values that do not change: rent exemption for a given size, a mint's decimals, and PDA derivations. Anything that changes per slot needs a refresh strategy rather than a TTL.

Should I cache getLatestBlockhash? Not as a TTL cache. Refresh it on a background timer so the hot path never makes a network call and never serves a value past its validity window.

Why do I get Blockhash not found with caching enabled? Because a cached blockhash outlived its roughly 150-block window. A background refresher avoids this by always holding a recent value rather than expiring an old one.

What is minContextSlot for? Refusing responses from nodes behind a slot you have already seen. It prevents a provider's node pool from serving you a view that appears to move backwards.

Why do my balances sometimes go backwards? Consecutive requests hit different nodes in a pool at different slots. Pass minContextSlot with the highest slot you have observed so the node errors instead of returning stale data.

Should commitment be part of my cache key? Yes. The same account read at processed and finalized returns different values, so a key without it serves whichever response arrived first.

Can I cache signature statuses? No. You poll them because the answer is expected to change, so a cached status produces a confirmation loop that never resolves.

Is it safe to cache account data? Briefly, for display. Not for sizing a trade — a decision made against cached state will be rejected by the on-chain check that runs against current state.

Can I cache simulation results? No. Simulation runs against the current slot, so a cached result describes a state that has already passed and tells you nothing about the slot you will land in.

Should I cache that an account does not exist? Briefly at most, and never for addresses expected to appear. A token account that is missing now is routinely created moments later, and a cached absence outlives the truth.

How long should I cache account data? Shorter than the time it takes for the value to matter. If a stale value would change a decision, it should not be cached at all — put the check on chain instead.

Does caching help with rate limits? Substantially, for repeated reads of values that do not change. Most rate-limit pressure comes from re-fetching constants and from polling, both of which caching or subscriptions address.

What should I cache instead of polling? Nothing — replace the polling itself with a subscription. accountSubscribe pushes changes, which is both cheaper than polling and fresher than any cache.

Should I cache PDA derivations? Yes, indefinitely. The same seeds always produce the same address, and the derivation runs a search loop, so caching removes repeated CPU for an unchanging answer.

Do dataSlice and filters need to be in the cache key? Yes. Any parameter that changes the response must be part of the key, or you will serve a slice of the data to a caller that asked for all of it.

Where should the real safety check live? On chain, inside the instruction. A minimum output enforced by the program cannot be stale, which is what makes cached inputs acceptable for the decision beforehand.

← Back to all posts