> ## Documentation Index
> Fetch the complete documentation index at: https://docs.lucidblock.xyz/llms.txt
> Use this file to discover all available pages before exploring further.

# Performance

> Freshness guarantees, RPC cost and measured latency.

How fast a response is, how old its data can be, and what a request costs in Solana RPC calls. The numbers in the
[measured table](https://docs.lucidblock.xyz/reference/performance.md#measured-latency) come from `pnpm bench:api`; the guarantees above it come from the configuration
and the code, not from a measurement.

## Freshness guarantees

* **Snapshot TTL: 120 s by default.** A wallet's snapshot is "fresh" while `freshness.age_seconds` ≤
  `freshness.max_age_seconds`, which is the request's `freshness_seconds` (0…86,400, default 120,
  `SNAPSHOT_FRESHNESS_S`). A fresh snapshot is served as is.
* **Stale while refreshing (max 10 minutes).** A snapshot older than the freshness target but at most 10 minutes old (`MAX_STALE_SERVE_S`) is served at once (`200`); anything older is never served and the request returns `202` with a fresh scan. A served stale snapshot carries `freshness.stale: true`,
  `coverage.status: "stale"`, `refreshing: true` and a `refresh_job_id`, while one bounded refresh scan runs in the
  background. A stale snapshot never has a `total_value_usd` (`coverage_incomplete`). Request again a few seconds
  later for the new snapshot. With no snapshot at all, the first request returns `202` and a job to poll
  ([the async flow](https://docs.lucidblock.xyz/guides/polling.md#the-async-flow-202)).
* **`?wait=N` long-poll (0…10 s).** On a cold or stale request the API waits up to `N` seconds for the scan it just
  started and returns the fresh `200` if it lands in time (else the usual `202` or stale `200`). The wait reads only
  the job status from storage, never the chain, so it adds no RPC cost. `?wait=8` covers most cold scans (see below).
* **`freshness_seconds`** lets you trade freshness for speed: a larger value accepts older snapshots and avoids scans;
  `0` always triggers a refresh (and can hit the cold-scan limit, see [Rate limits](https://docs.lucidblock.xyz/reference/rate-limits.md)).
* **Prices refresh every 2 minutes** (a scheduled job), not per request. Each leg shows `price_as_of`. A built-in
  on-chain price stays `priced` for up to 300 s and a TokenPrices price for up to 900 s; older prices are `stale`
  (shown, counted in no total) up to 24 h, and not served after that. LST redemption rates are stale from an epoch
  lag of 3.
* **Lending interest** is valued as of each reserve's last on-chain refresh (the leg's `context.as_of_slot`), not
  projected forward; an old reserve sets the `interest_stale` flag.
* **Slots.** `observed_slots` is the slot range of every RPC read in the scan. `snapshot_consistency: "multi_slot"` is
  normal: a scan's reads are not one atomic read, so they can span a few slots.

## RPC cost per request

* **Cached requests make 0 RPC calls.** A request answered from a snapshot is read from the database only. A stale or
  missing snapshot also triggers one background scan for the wallet (deduplicated: concurrent requests share it).
* **A cold or refresh scan** reads the wallet's token accounts (SPL Token and Token-2022), its native SOL balance and
  each supported protocol's accounts. It is capped at **40 RPC HTTP requests** per scan (`SCAN_MAX_REQUESTS`, a batch
  counts once, retries included) and 20 s; a protocol that would exceed its own share is reported as an error, never
  as an empty result. The [measured table](https://docs.lucidblock.xyz/reference/performance.md#measured-latency) shows what scans actually use.
* **Prices are never fetched per request.** They come from the 2-minute refresh; scans read the stored prices.

## Measured latency

**Local measurement, 2026-11-14, not production.** `pnpm bench:api` against `wrangler dev` on one laptop (local
D1, live mainnet RPC through the project's keyed endpoint), the 7 sample wallets of the viewer. Production latency
adds the network path to Cloudflare and can differ either way; re-measured after each deploy.

| Measure | n | p50 | p90 | p95 | p99 |
| - | - | - | - | - | - |
| Cached `GET /defi-positions` (snapshot served from storage) | 500 | 6.4 ms | 8.4 ms | 9.5 ms | 13.8 ms |
| Cold scan, first `202` to first `200` (polling every 250 ms) | 7 | 4.0 s | 6.5 s | 6.5 s | 6.5 s |

**`?wait=8` spot check (local, 2026-11-14, not production):** two never-scanned sample wallets each returned `200` in
one call, in 4.7 s and 4.0 s (live mainnet RPC through the keyed endpoint, local D1). Consistent with the cold-scan
p90 above, `?wait=8` returns the snapshot directly for most cold wallets; slower scans fall back to `202`.

| RPC use per scan (from `rpc_usage`) | n | min | p50 | max |
| - | - | - | - | - |
| Cold scan: RPC HTTP requests (cap 40) | 7 | 19 | 24 | 29 |
| Cold scan: billed calls (batch members) | 7 | 20 | 25 | 30 |
| Warm rescan (shared caches hot): RPC HTTP requests | 3 | 18 | 20 | 21 |

| Age at read time (same run) | n | p50 | max |
| - | - | - | - |
| Snapshot age (`freshness.age_seconds`) | 500 | 18 s | 33 s |
| Leg price age (`now − price_as_of`) | 6 | 48 s | 48 s |

The ages reflect this short local run (scans a few seconds to a minute old, one price refresh at the start). In
production the price refresh runs every 2 minutes, so a served price is normally under about 2 minutes old plus chain
finality, and a served snapshot is at most `freshness_seconds` old unless it is marked stale.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.