# SVM JSON-RPC cache > Source: https://docs.erpc.cloud/config/database/svm-json-rpc-cache > Cache Solana JSON-RPC responses without ever serving a stale account balance — eRPC classifies every SVM read by Solana's own commitment rules, keys base58 params case-sensitively, and refuses to treat a moving rooted head as immutable. > Format: machine-readable markdown export of the docs page above. > All collapsible AI sections are inlined and fully expanded. # SVM JSON-RPC cache Solana's `finalized` commitment is **not** an immutability guarantee — it is the state at the latest *rooted* slot, and that slot advances roughly every 400 ms. A cache that reads "finalized" as "safe forever" will happily serve a two-hour-old `getBalance` result. eRPC's SVM cache is built around that single fact: only responses pinned to a specific past slot or signature are cached as immutable; everything that tracks the rooted head is treated as realtime and bounded by a TTL. **What you get** - Finality classification that follows Solana commitment semantics, not EVM block-depth semantics - Case-preserving cache keys — two base58 pubkeys differing only in letter case never collide - Effectful and sub-slot methods (`sendTransaction`, `requestAirdrop`, `getLatestBlockhash`, …) hard-excluded from both reads and writes, regardless of policy - The same connectors, policies, and zstd compression as the EVM cache, on a disjoint key namespace ## Quick taste Illustrative, not a tuned production config — cache slot-pinned finalized reads forever, everything else briefly: **Config path:** `database.svmJsonRpcCache` **YAML — `erpc.yaml`:** ```yaml database: svmJsonRpcCache: connectors: - id: mem driver: memory memory: { maxItems: 100000 } policies: # getBlock / getTransaction at commitment:finalized are pinned to a slot # that is already rooted — genuinely immutable, safe to keep forever. - connector: mem network: "*" method: "*" finality: finalized ttl: 0 # Everything else tracks the rooted head. TTL is the ONLY staleness bound. - connector: mem network: "*" method: "*" finality: realtime ttl: 2s ``` **TypeScript — `erpc.ts`:** ```typescript database: { svmJsonRpcCache: { connectors: [{ id: "mem", driver: "memory", memory: { maxItems: 100000 } }], policies: [ // getBlock / getTransaction at commitment:finalized are pinned to a slot // that is already rooted — genuinely immutable, safe to keep forever. { connector: "mem", network: "*", method: "*", finality: "finalized", ttl: 0 }, // Everything else tracks the rooted head. TTL is the ONLY staleness bound. { connector: "mem", network: "*", method: "*", finality: "realtime", ttl: "2s" }, ], }, } ``` ## Agent reference Copy one of these prompts into your AI agent session (Claude Code, Cursor, …) — each one points the agent at this page's machine-readable reference so it can do the work correctly: **Prompt Example #1: set up SVM caching from scratch** ```text I want to cache Solana JSON-RPC responses in eRPC. Add a database.svmJsonRpcCache block with an in-memory connector and one policy per finality bucket (finalized, unfinalized, realtime), with TTLs that respect Solana commitment semantics — remember that commitment:finalized state reads are moving-head, not immutable. Work with my existing eRPC config. Read the full reference first: https://docs.erpc.cloud/config/database/svm-json-rpc-cache.llms.txt ``` **Prompt Example #2: explain why getBalance is never cached permanently** ```text My eRPC SVM cache has a catch-all policy with finality: finalized and ttl: 0, but getBalance and getAccountInfo requests still hit upstreams every time even when I pass commitment: finalized. Explain the finality classification eRPC applies to SVM methods and tell me which policies I am missing. Reference: https://docs.erpc.cloud/config/database/svm-json-rpc-cache.llms.txt ``` **Prompt Example #3: share one Redis backend between EVM and SVM networks** ```text I run both EVM and SVM networks in one eRPC project and want a single Redis connector behind both caches without key collisions. Set up database.evmJsonRpcCache and database.svmJsonRpcCache pointing at the same Redis connector definition and explain why the keyspaces stay disjoint. Reference: https://docs.erpc.cloud/config/database/svm-json-rpc-cache.llms.txt ``` **Prompt Example #4: cache large getBlock payloads without blowing up storage** ```text Solana getBlock responses in my deployment are multiple megabytes. Configure database.svmJsonRpcCache so finalized getBlock/getTransaction results are cached permanently in Redis with zstd compression, and add a size limit so pathological responses are skipped rather than stored. Reference: https://docs.erpc.cloud/config/database/svm-json-rpc-cache.llms.txt ``` --- ### SVM JSON-RPC cache — full agent reference ### How it works **Separate config block, shared machinery.** SVM caching is configured under `database.svmJsonRpcCache` and takes the *same* `CacheConfig` schema as [`database.evmJsonRpcCache`](/config/database/evm-json-rpc-cache.llms.txt) — identical `connectors`, `policies`, and `compression` sub-blocks, identical [drivers](/config/database/drivers.llms.txt). The two blocks are independent: an EVM-only config is unaffected, and an SVM network with no `svmJsonRpcCache` block simply never caches. `SvmJsonRpcCache` implements the same `common.CacheDAL` interface as its EVM counterpart, so the network layer calls it through the same seam. **Key namespaces are disjoint by construction.** The partition key is prefixed with the network id (`svm:mainnet-beta`, `svm:fogo:mainnet`, `evm:1`), so pointing both caches at one Redis or DynamoDB connector is safe and is the intended deployment for mixed projects. **Finality classification — the load-bearing part.** `GetFinality` resolves an SVM request to a `DataFinalityState` in four ordered steps: | Step | Predicate | Result | |---|---|---| | 1 | `neverCacheMethods` | `realtime` — *and* hard-skipped by the cache layer | | 2 | `alwaysFinalizedMethods` | `finalized` (immutable by construction) | | 3 | `slotPinnedMethods` **and** effective commitment is `finalized` | `finalized` | | 4 | `slotPinnedMethods` at any weaker commitment | `unfinalized` | | 5 | everything else | `realtime` (moving-head read) | Step 5 is the rule that matters, and it is why step 3 needs an explicit table at all. **`commitment: finalized` on Solana means "the state at the latest ROOTED slot", and the rooted slot advances roughly every 400 ms — it is a moving head, not an immutability promise.** `getBalance`, `getAccountInfo`, `getProgramAccounts`, and `getTokenAccountBalance` at `finalized` therefore answer a *different question every slot*, exactly like EVM's `latest` block tag. Classifying them `finalized` would be a permanent-cache bug: `finalized` is the zero value of the Go enum that a policy with no explicit `finality` matches, and an unset TTL means "no expiry" in every connector — so a later transfer would never invalidate the cached balance. Only a response pinned to an explicit positional identifier — a slot number or a transaction signature — is genuinely immutable once that slot is rooted. That is the whole content of `slotPinnedMethods`: ``` slotPinnedMethods = getBlock, getTransaction ``` At `finalized` these are promoted to `finalized`. Below `finalized` they are `unfinalized`: still pinned to a slot, but a not-yet-rooted slot can be dropped by a minority-fork switch, so the answer can still change. ``` alwaysFinalizedMethods = getInflationReward (defined only over finalized epochs) getBlockTime (takes no commitment param; stable once the slot exists) ``` ``` neverCacheMethods = sendTransaction, sendRawTransaction, simulateTransaction, requestAirdrop, getLatestBlockhash, getRecentBlockhash, getFeeForMessage, getSignatureStatuses, getVoteAccounts, getLeaderSchedule, getEpochInfo, getSlotLeaders, getRecentPerformanceSamples, getRecentPrioritizationFees ``` Two categories live in `neverCacheMethods`: **mutating or effectful** calls, where caching would break the at-least-once semantics callers expect, and **transient realtime snapshots** that go stale in under one slot. `getEpochSchedule` is deliberately *excluded* — its constants only change at epoch boundaries (~432,000 slots), so it falls through to the moving-head bucket and is cached under the realtime policy's TTL. **Never-cache is hard-enforced, not policy-driven.** `realtime` is still a *cacheable* finality at the policy layer, so mapping these methods to `realtime` is not sufficient on its own. `Get` and `Set` both check `neverCacheMethods` **before** policy matching and return early. An operator's stray `finality: realtime` catch-all therefore cannot cache `sendTransaction`. **Finality reflects the commitment eRPC pinned.** Step 3 calls `resolveCommitment` — the same predicate the [commitment-injection hook](/reference/svm/commitment.llms.txt) uses — so classification tracks the level eRPC actually pinned on the request, not the level a node happens to default to. When nothing is pinned (no caller `commitment`, no `svm.commitment`, or injection legitimately skips the legacy encoding-string form), the response is classified `unfinalized`. That is deliberately stricter than the routing predicate `IsFinalizedCommitment`, which follows the node's documented `finalized` default: cache permanence is a one-way door — `finality: finalized` is the zero value an unset-TTL policy matches, so a wrong promotion pins a value forever while a wrong demotion costs one re-fetch. Set `svm.commitment` to have unpinned slot-addressed reads cached as final. **Cache-key derivation.** Two keys, as in EVM, but both dimensions differ: - **Partition key: `:`, or `::` when `networks[].cacheKeySuffix` is set.** `slotRef` is the request's `minContextSlot` when one is present in any object param, otherwise the literal `"*"`. This replaces EVM's `blockRef` dimension. Because `slotRef` is derived purely from the request params, a given `(method, params)` tuple always produces the same partition key on both `Set` and `Get` — so SVM always reads `ConnectorMainIndex` and never needs EVM's reverse-index wildcard fallback. When `:` is itself a valid network id (`svm:` plus a suffix), colons in the network id are escaped (`svm\:mainnet-beta:systx:*`) so the partition is not the unsuffixed key of that longer id. Unsuffixed keys are unchanged. - **Request key: `:`.** Derived by `svmRequestKey`, **not** by the shared `req.CacheHash()`. **Cache keys are case-preserving, and this is a correctness requirement.** The shared EVM hasher lowercases every string param, which is the right normalization for hex but catastrophic for Solana: base58 pubkeys and transaction signatures are **case-sensitive**, so two distinct valid accounts differing only by letter case would collapse onto one key and be served each other's data. `svmRequestKey` preserves case exactly. It marshals params with `encoding/json` (not sonic) on purpose — the standard library documents that it sorts map keys, which is what makes the key deterministic across runs and across Go map iteration order, and its grammar is both type- and structure-delimiting, so `"abc"`, `["a","bc"]`, and `{"a":"bc"}` cannot collide, nor can the number `1` and the string `"1"`. The method name is both hashed and prefixed, so a method name containing `:` cannot forge another method's key. Params `nil` and `params: []` normalize to the same key. **No block-timestamp age guard.** EVM re-checks a realtime hit's block timestamp against the policy TTL before accepting it. SVM has no equivalent: because only genuinely immutable responses are classified `finalized`, the **TTL on the realtime policy is the staleness bound** — the same lever EVM uses for `latest`. There is consequently no `erpc_cache_get_age_guard_reject_total` traffic on SVM networks. **Get is sequential, not a parallel fan-out.** `Get` walks the matched policies in list order and returns the first non-empty hit; there is no goroutine race, no `fanCtx`, and no peer cancellation. **Order your policies fastest-connector-first** — with SVM this is not a tie-break hint, it is the actual probe order. `Set` likewise writes to matched connectors sequentially. **Writes.** Responses carrying a JSON-RPC `error` body are never stored. Size limits gate on the **original** (uncompressed) payload — they express a response-size ceiling, not a storage-footprint one. zstd compression runs at most once per request and only after some policy has accepted the payload, so a multi-megabyte `getBlock` that every policy rejects on size never pays for compression. **Compression.** Identical to the EVM cache: enabled by default (the `compression` block is auto-created even when omitted), `threshold` default 1024 bytes, and detection on read is by the zstd magic bytes `0x28 0xB5 0x2F 0xFD` — so entries written while compression was enabled stay readable after it is turned off. Solana `getBlock` payloads routinely exceed a megabyte, so leaving compression on is the right default. ### Config schema All fields live under `database.svmJsonRpcCache` and use the **same `CacheConfig` schema as `database.evmJsonRpcCache`** — see the [EVM cache page's config schema](/config/database/evm-json-rpc-cache.llms.txt) for the full field-by-field tables of `connectors[*]`, `policies[*]`, and `compression`. Only the SVM-specific semantics are restated here. | Field | Type | Default | SVM-specific behavior | |---|---|---|---| | `database.svmJsonRpcCache` | `*CacheConfig` | `nil` (no SVM caching) | Independent of `evmJsonRpcCache`. Validated at startup; a broken block fails config validation rather than silently disabling the cache. | | `policies[*].finality` | enum | `finalized` (Go zero value) | `finalized` matches only `getBlock`/`getTransaction` at commitment `finalized`, plus `getInflationReward`/`getBlockTime`. **Every other SVM read is `realtime`.** | | `policies[*].ttl` | Duration | `0` (no expiry) | On a `realtime` SVM policy this is the *only* staleness bound — there is no block-timestamp age guard to catch an over-long TTL. | | `policies[*].network` | string | `"*"` | Matches the SVM network id: `svm:` or `svm::`. | | `policies[*].method` | string | `"*"` | Matches Solana method names (`getBlock`, `getAccountInfo`, …). Methods in `neverCacheMethods` are excluded before matching and cannot be re-enabled by any policy. | ### Worked examples **1. The three-bucket baseline.** `finalized` for the two slot-pinned reads, `unfinalized` for those same reads below `finalized` commitment, and `realtime` for everything else. Without the `realtime` policy the overwhelming majority of SVM traffic — every account and token read — bypasses the cache entirely: **Config path:** `database.svmJsonRpcCache` **YAML — `erpc.yaml`:** ```yaml database: svmJsonRpcCache: connectors: - id: mem driver: memory memory: { maxItems: 500000, maxTotalSize: 4GB } policies: # Immutable: getBlock/getTransaction pinned to a rooted slot, plus # getBlockTime and getInflationReward. - connector: mem network: "*" method: "*" finality: finalized ttl: 0 # getBlock/getTransaction at confirmed/processed — pinned to a slot, but a # fork switch can still drop it. ~2 slots of tolerance. - connector: mem network: "*" method: "*" finality: unfinalized ttl: 800ms # Everything else: getBalance, getAccountInfo, getProgramAccounts, getSlot, # getBlockHeight, getSignaturesForAddress, ... all moving-head reads. - connector: mem network: "*" method: "*" finality: realtime ttl: 2s ``` **TypeScript — `erpc.ts`:** ```typescript database: { svmJsonRpcCache: { connectors: [{ id: "mem", driver: "memory", memory: { maxItems: 500000, maxTotalSize: "4GB" } }], policies: [ // Immutable: getBlock/getTransaction pinned to a rooted slot, plus // getBlockTime and getInflationReward. { connector: "mem", network: "*", method: "*", finality: "finalized", ttl: 0 }, // getBlock/getTransaction at confirmed/processed — pinned to a slot, but a // fork switch can still drop it. ~2 slots of tolerance. { connector: "mem", network: "*", method: "*", finality: "unfinalized", ttl: "800ms" }, // Everything else: getBalance, getAccountInfo, getProgramAccounts, getSlot, // getBlockHeight, getSignaturesForAddress, ... all moving-head reads. { connector: "mem", network: "*", method: "*", finality: "realtime", ttl: "2s" }, ], }, } ``` **2. Two tiers: memory in front, Redis for the immutable pool.** Historical `getBlock`/`getTransaction` results are large, read repeatedly, and never change once rooted — they belong in a durable shared tier that survives restarts. Moving-head reads have a 2 s lifetime and never justify a network round-trip. Note that SVM probes policies **in list order**, so the memory connector must come first: **Config path:** `database.svmJsonRpcCache` **YAML — `erpc.yaml`:** ```yaml database: svmJsonRpcCache: connectors: - id: mem driver: memory memory: { maxItems: 200000, maxTotalSize: 2GB } - id: redis driver: redis redis: uri: "redis://\${REDIS_HOST}:6379/0" policies: # Memory FIRST: Get walks policies in order and returns the first hit — # unlike EVM there is no parallel race, so order IS the probe order. - connector: mem network: "*" method: "getBlock|getTransaction" finality: finalized ttl: 0 - connector: redis network: "*" method: "getBlock|getTransaction" finality: finalized # Durable and permanent: a rooted slot's block never changes. ttl: 0 # Moving-head reads stay local — a Redis RTT costs more than the upstream # call it would save at a 2s TTL. - connector: mem network: "*" method: "*" finality: realtime ttl: 2s ``` **TypeScript — `erpc.ts`:** ```typescript database: { svmJsonRpcCache: { connectors: [ { id: "mem", driver: "memory", memory: { maxItems: 200000, maxTotalSize: "2GB" } }, { id: "redis", driver: "redis", redis: { uri: "redis://\${REDIS_HOST}:6379/0" } }, ], policies: [ // Memory FIRST: Get walks policies in order and returns the first hit — // unlike EVM there is no parallel race, so order IS the probe order. { connector: "mem", network: "*", method: "getBlock|getTransaction", finality: "finalized", ttl: 0 }, { connector: "redis", network: "*", method: "getBlock|getTransaction", finality: "finalized", // Durable and permanent: a rooted slot's block never changes. ttl: 0, }, // Moving-head reads stay local — a Redis RTT costs more than the upstream // call it would save at a 2s TTL. { connector: "mem", network: "*", method: "*", finality: "realtime", ttl: "2s" }, ], }, } ``` **3. One Redis backend behind both architectures.** A mixed EVM + SVM project can share storage. The network-id prefix on every partition key (`evm:1:…` vs `svm:mainnet-beta:…`) keeps the namespaces disjoint, so no coordination is needed beyond declaring the connector in both blocks: **Config path:** `database` **YAML — `erpc.yaml`:** ```yaml database: evmJsonRpcCache: connectors: - id: redis driver: redis redis: uri: "redis://\${REDIS_HOST}:6379/0" policies: - connector: redis network: "*" method: "*" finality: finalized ttl: 0 svmJsonRpcCache: # Same physical Redis. Partition keys are prefixed with the network id, so # evm:1 and svm:mainnet-beta entries cannot collide. connectors: - id: redis driver: redis redis: uri: "redis://\${REDIS_HOST}:6379/0" policies: - connector: redis network: "*" method: "getBlock|getTransaction" finality: finalized ttl: 0 ``` **TypeScript — `erpc.ts`:** ```typescript database: { evmJsonRpcCache: { connectors: [{ id: "redis", driver: "redis", redis: { uri: "redis://\${REDIS_HOST}:6379/0" } }], policies: [{ connector: "redis", network: "*", method: "*", finality: "finalized", ttl: 0 }], }, svmJsonRpcCache: { // Same physical Redis. Partition keys are prefixed with the network id, so // evm:1 and svm:mainnet-beta entries cannot collide. connectors: [{ id: "redis", driver: "redis", redis: { uri: "redis://\${REDIS_HOST}:6379/0" } }], policies: [{ connector: "redis", network: "*", method: "getBlock|getTransaction", finality: "finalized", ttl: 0 }], }, } ``` **4. Slot-partitioned caching for monotonic readers.** Clients that pass `minContextSlot` to enforce read-your-writes get a partition key per slot floor, so a request at `minContextSlot: N` never reads an entry stored for a different floor. This is a natural fit for indexers that advance a cursor; it also means a client that varies `minContextSlot` on every call gets a fresh partition every time and effectively bypasses the cache: **Config path:** `database.svmJsonRpcCache` **YAML — `erpc.yaml`:** ```yaml database: svmJsonRpcCache: connectors: - id: mem driver: memory memory: { maxItems: 1000000, maxTotalSize: 4GB } policies: - connector: mem network: "svm:mainnet-beta" method: "*" finality: realtime # High item count: a distinct minContextSlot value means a distinct # partition key, so key cardinality scales with distinct slot floors. ttl: 2s ``` **TypeScript — `erpc.ts`:** ```typescript database: { svmJsonRpcCache: { connectors: [{ id: "mem", driver: "memory", memory: { maxItems: 1000000, maxTotalSize: "4GB" } }], policies: [{ connector: "mem", network: "svm:mainnet-beta", method: "*", finality: "realtime", // High item count: a distinct minContextSlot value means a distinct // partition key, so key cardinality scales with distinct slot floors. ttl: "2s", }], }, } ``` ### Request/response behavior - **Cache key structure.** Partition key `:`, or `::` when `networks[].cacheKeySuffix` is set. `:` and `\` inside `slotRef` are backslash-escaped; other values, including `"*"`, are stored as-is. When `:` is itself a valid network id, colons in the network id are escaped instead. `slotRef` is the request's `minContextSlot` or `"*"`. Request key `:`, case-preserving. Always read from `ConnectorMainIndex` — no reverse index. - **`neverCacheMethods` short-circuit both directions.** `Get` returns a miss and `Set` returns without writing, before any policy is consulted. - **Responses with a JSON-RPC `error` field are never cached.** Native Solana error codes and `error.data` are preserved to the client instead — see the [error reference](/reference/errors.llms.txt#svm-solana-error-contract). - **A `getSlot` response is corrected after the cache read.** The post-forward hook enforces the highest known slot for the request's commitment even on a cache hit, so a cached value can never make the slot number appear to move backwards. See [slot tracking](/reference/svm/slot-tracking.llms.txt). - **No realtime age gate.** A `realtime` hit is accepted on TTL alone. There is no block-timestamp cross-check and no `ttl_rejected` outcome on SVM. - **`X-ERPC-Skip-Cache-Read`** works identically to EVM — `"true"` skips every connector, a glob pattern skips matching connector ids. ### Best practices - **Always define a `realtime` policy.** It is not an edge case on SVM — it is the majority of your traffic. `getBalance`, `getAccountInfo`, `getProgramAccounts`, `getTokenAccountBalance`, `getSlot`, `getBlockHeight`, and `getSignaturesForAddress` are all `realtime` at *every* commitment level, including `finalized`. A config with only a `finality: finalized` policy caches almost nothing. - **Never "fix" a low hit rate by forcing state reads to `finalized`.** There is no config knob to do this on SVM, and that is deliberate — it would be a permanent-cache correctness bug, not a tuning win. If you need higher hit rates on account reads, raise the `realtime` TTL knowingly. - **Keep `realtime` TTL in the 1–3 s range.** One Solana slot is ~400 ms. A 2 s TTL means a caller may observe state up to ~5 slots old; anything much larger starts to surface visibly stale balances. - **Set `unfinalized` TTL to roughly two slots (≈800 ms).** That bucket only ever holds `getBlock`/`getTransaction` below `finalized` commitment, where a fork switch is the risk being bounded. - **Order policies fastest-connector-first.** SVM's `Get` is a sequential walk, so a slow remote connector listed before a local memory connector adds its full latency to every hit. - **Leave compression enabled.** `getBlock` payloads regularly exceed a megabyte; zstd at the default threshold pays for itself immediately. - **Point EVM and SVM at the same connector definitions** in a mixed project. The keyspaces cannot collide, and one storage tier is one thing to operate. ### Edge cases & gotchas 1. **`finality: finalized` is the Go zero value — omitting `finality` creates a finalized-only policy.** On SVM this is far more surprising than on EVM, because the `finalized` bucket holds only four methods (`getBlock`, `getTransaction` at `finalized` commitment, plus `getInflationReward` and `getBlockTime`). A catch-all policy with no explicit `finality` will look like the cache is broken. 2. **`commitment: finalized` does not promote a state read.** `getBalance(pubkey, {commitment: "finalized"})` is `realtime`, by design. Solana `finalized` is the latest *rooted* slot — a head that moves every ~400 ms — so the answer changes continuously. 3. **`minContextSlot` is not a promotion signal either.** Per the Solana RPC reference it is the minimum bank slot at which the request may be *evaluated* — a node-freshness floor, not a lower bound on returned history. `getBalance(pubkey, {minContextSlot: 1})` still answers at the current head. It affects the cache *partition key* only. 4. **Cache keys are case-sensitive.** This is the opposite of the EVM cache, which lowercases string params. Base58 is case-significant, so `getAccountInfo("Abc…")` and `getAccountInfo("abc…")` are different accounts and get different keys. Do not normalize pubkey case in a client layer expecting eRPC to fold it. 5. **`getSignaturesForAddress` is `realtime`, deliberately.** The signature list for an address *grows* as new transactions land, so it tracks the head even though it names an address rather than a slot. 6. **`getBlocks` / `getBlocksWithLimit` are `realtime`, and a fully-historical range is not promoted.** `getBlocks(start)` with no end slot runs to the current head, and either form can name an upper bound the chain has not reached yet. A fully-in-the-past range *is* immutable and only gets realtime-TTL caching here; promoting it would make finality depend on mutable poller state and therefore vary over time for an identical request. `getBlocks` is cheap next to `getBlock`. 7. **`getEpochInfo` is never cached but `getEpochSchedule` is.** `getEpochInfo` carries the live slot index; `getEpochSchedule` returns epoch constants that change only at epoch boundaries. 8. **`getBlockTime` is treated as final by construction.** A slot's timestamp can in principle change if a not-yet-rooted slot is dropped. This is a low-risk simplification, not a guarantee. 9. **A `realtime` policy cannot resurrect a `neverCacheMethods` entry.** The hard skip runs before policy matching in both `Get` and `Set`. 10. **`params: null` and `params: []` share one key.** They are the same call; the encoder normalizes them so `json.Marshal` cannot emit `null` for one and `[]` for the other. 11. **No per-connector write timeout.** Unlike the EVM cache's hard 5 s `Set` ceiling, SVM `Set` relies on the connector's own configuration and the request context. Configure `failsafeForSets` on the connector if you need a bound. 12. **Constructor failures are downgraded to a warning at startup.** `erpc/init.go` logs a WARN and continues if `NewSvmJsonRpcCache` fails, which would leave the cache silently absent — this is exactly why the config block is schema-validated eagerly at startup instead. 13. **Same-chain networks share partition keys unless `cacheKeySuffix` is set.** Same rule as EVM: empty suffix keeps `:`. A `:` or `\` inside `slotRef` (a string `minContextSlot` is passed through) is backslash-escaped so the key cannot collide with another suffix split. A suffix on `svm:` would otherwise spell the unsuffixed key of `svm::`, which is a valid network id, so that case escapes the network id's colons (`svm\:::`). A suffix on `svm::` cannot spell a network id, and keeps `::`. Source: [`architecture/svm/json_rpc_cache.go`](https://github.com/erpc/erpc/blob/main/architecture/svm/json_rpc_cache.go), [`common/config.go`](https://github.com/erpc/erpc/blob/main/common/config.go). 14. **Upgrading the partition encoding does not remove collided entries.** Previously, `svm:foo` with suffix `bar` and unsuffixed `svm:foo:bar` both used `svm:foo:bar:`. The new suffixed key changes, but the unsuffixed reader still accepts old entries, including responses from the other network. If these networks shared a cache, stop all old-version writers before purging the affected partitions from every configured cache connector. Alternatively, move both networks to clean, isolated namespaces. Do not resume affected cache reads until cleanup or isolation is complete. A rolling upgrade or suffixed-cache refill alone is insufficient. ### Observability The SVM cache emits the **same `erpc_cache_*` metric family** as the EVM cache, so existing dashboards work unchanged. The `network` label distinguishes SVM traffic (`svm:mainnet-beta`, …). | Metric | Type | When it fires | |---|---|---| | `erpc_cache_get_success_hit_total` | counter | A policy returned a non-empty stored value | | `erpc_cache_get_success_miss_total` | counter | Every matched policy missed | | `erpc_cache_get_skipped_total` | counter | No policy matched the request's network/method/finality | | `erpc_cache_get_error_total` | counter | A connector `Get` errored; the walk continues to the next policy | | `erpc_cache_set_skipped_total` | counter | Payload rejected by a policy's size limits | | `erpc_cache_set_error_total` | counter | A connector `Set` errored (logged at WARN, swallowed) | | `erpc_cache_set_original_bytes` / `erpc_cache_set_compressed_bytes` | counter | Pre- and post-zstd payload accounting; the compressed counter only advances when compression actually shrank the value | | `erpc_cache_get_success_hit_duration` / `erpc_cache_get_success_miss_duration` | histogram | Hit and miss latency | **Notably absent on SVM:** `erpc_cache_get_age_guard_reject_total` never fires — there is no block-timestamp age gate. **Debug/warn logs:** `"svm cache get failed; trying next policy"` (DEBUG, per-policy connector error) and `"svm cache set failed"` (WARN, carries `groupKey` and `requestKey`). ### Source code entry points - [`architecture/svm/json_rpc_cache.go`](https://github.com/erpc/erpc/blob/main/architecture/svm/json_rpc_cache.go) — `SvmJsonRpcCache`: `Get`/`Set`, `generateKeys`, `svmRequestKey` (case-preserving hashing), `extractSlotRef`, zstd compress/decompress, `doGet` main-index rationale. - [`architecture/svm/finality.go`](https://github.com/erpc/erpc/blob/main/architecture/svm/finality.go) — `GetFinality`, `neverCacheMethods`, `alwaysFinalizedMethods`, `slotPinnedMethods`, `IsFinalizedCommitment`, and the moving-head rationale in full. - [`common/config.go — DatabaseConfig.SvmJsonRpcCache`](https://github.com/erpc/erpc/blob/main/common/config.go#L273-L275) - [`common/validation.go — eager svmJsonRpcCache validation`](https://github.com/erpc/erpc/blob/main/common/validation.go#L258-L266) ### Related pages - [EVM JSON-RPC cache](/config/database/evm-json-rpc-cache.llms.txt) — the shared `CacheConfig` schema in full: connectors, policies, compression, size limits. - [Storage connectors](/config/database/drivers.llms.txt) — memory, Redis, DynamoDB, PostgreSQL driver options. - [SVM commitment & finality](/reference/svm/commitment.llms.txt) — how the effective commitment is resolved and injected. - [SVM slot tracking & health](/reference/svm/slot-tracking.llms.txt) — the state poller, `getSlot` correction, and ingestion-lag cordoning. - [Networks](/config/projects/networks.llms.txt) — declaring an SVM network and its `svm.*` options. - [Error taxonomy](/reference/errors.llms.txt#svm-solana-error-contract) — the SVM error contract and missing-data family. --- ## Navigation (machine-readable surface) - Up: [All pages index](https://docs.erpc.cloud/llms.txt) - Root index of every page: [llms.txt](https://docs.erpc.cloud/llms.txt) · everything in one file: [llms-full.txt](https://docs.erpc.cloud/llms-full.txt) ### Sibling pages - [Storage drivers](https://docs.erpc.cloud/config/database/drivers.llms.txt) — Five interchangeable cache back-ends — memory, Redis, PostgreSQL, DynamoDB, and a read-only gRPC BDS connector — all behind one uniform interface, with optional per-operation failsafe policies that keep transient storage hiccups invisible to your upstreams. - [Cache policies](https://docs.erpc.cloud/config/database/evm-json-rpc-cache.llms.txt) — Stop paying for the same upstream call twice — eRPC caches every EVM JSON-RPC response by finality bucket, fans out reads in parallel, and rejects stale tip-of-chain data before it ever reaches your users. - [Goldsky cache (gRPC)](https://docs.erpc.cloud/config/database/goldsky-cache.llms.txt) — Plug Goldsky's hosted, read-only gRPC cache into eRPC — historical blocks, logs, transactions and receipts served from Goldsky's indexed chain data before your requests reach your upstreams — and Monad's MIP-16 eth_queryLogs and eth_queryTransactions served natively. - [Shared state](https://docs.erpc.cloud/config/database/shared-state.llms.txt) — Give every pod in your fleet the same real-time view of each upstream's block height — routing stays consistent as you scale out, with zero added latency on the request path.