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

# Caching

> How the Expo SDK caches resolved flows, the fail-safe resolution chain, and how to control or clear the cache.

Caching makes repeat presents instant and is the foundation of FlowPilot's offline resilience. A resolved flow is kept in memory and on disk, and the SDK resolves a placement by walking a fail-safe chain that prefers a warm cache and degrades gracefully when the network is slow or gone.

## The fail-safe resolution chain

Every present and prefetch resolves a placement through the same ordered chain. Each tier is tried until one yields a presentable flow:

| Tier | Source                        | Network | When it wins                                                                                                                      |
| ---- | ----------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------- |
| 0    | Fresh cache hit               | none    | A cached flow is still within its freshness TTL. Presented instantly.                                                             |
| 1    | Live resolve                  | yes     | No fresh cache. Bounded by `resolveTimeout` (default 4s). On success, validated and cached.                                       |
| 2    | Stale cache (last-known-good) | none    | The live resolve failed or timed out, but an older cached flow exists (TTL ignored).                                              |
| 3    | Bundled default flow          | none    | Everything above missed. Renders a flow you shipped in the app. See [Offline and bundled flows](/expo-sdk/offline-bundled-flows). |

If no tier yields a presentable flow, the resolve fails, and you handle it with the [host fallback](/expo-sdk/presenting-placements#host-fallback-must-not-fail) (`resolveSession` returns `null`, or the presenter `fallback` renders).

Every analytics event carries the tier it was served from as `delivery_source` (`network`, `cache`, `stale_cache`, `bundled_default`), so offline and fallback renders are distinguishable in the dashboard. See [Analytics](/expo-sdk/analytics).

### The resolve deadline does not strand the cache

A live resolve that hits `resolveTimeout` degrades **the current caller** to a fallback tier, but it does **not** cancel the in-flight request. The resolve keeps running to populate the cache, so the next caller hits a warm Tier 0. A slow network therefore never hangs onboarding, and the cost of a slow first present is not wasted. Concurrent resolves for the same key are also coalesced into one round-trip, so a prefetch and a present that race share a single request.

### The no-fallback second chance

There is one case where the short `resolveTimeout` buys nothing: a present-time resolve where Tier 0 and Tier 2 both miss **and** no bundled default (Tier 3) is registered. Failing fast there would just strand the user on a blank screen, so the SDK instead waits up to `resolveNoFallbackTimeout` (default `12` seconds, floored at `resolveTimeout`) for the real flow rather than giving up at 4s. The still-in-flight resolve usually lands within that window (for example a cold backend that answers in \~6s). When a stale or bundled tier **is** available, the short `resolveTimeout` still applies, so a slow network degrades instantly. Set `resolveNoFallbackTimeout` at or below `resolveTimeout` to disable this second chance. See [Configuration](/expo-sdk/configuration).

## Cache freshness (TTL)

A cached flow is "fresh" for a number of seconds, after which Tier 0 misses and the SDK re-resolves (falling back to the same entry as stale cache if the network fails). The TTL comes from the resolve response's `cacheTtlSeconds`; when the backend omits it, the SDK uses the environment default:

| Environment   | Default TTL                                                        |
| ------------- | ------------------------------------------------------------------ |
| `development` | `0` (a fresh entry expires immediately, so effectively no caching) |
| `staging`     | `60` seconds                                                       |
| `production`  | `300` seconds                                                      |

<Note>
  Because `development` defaults to a `0` TTL, prefetching and "fast repeat present" have no visible effect there. Test caching behavior against `staging` or `production`. See [Configuration](/expo-sdk/configuration).
</Note>

## Identity-scoped cache keys

Cached flows are keyed by an **identity fingerprint**, a hash of the per-install user ID and any targeting attributes the resolve was personalized by. This guarantees a flow cached for one user (or one A/B variant) is never served to another after an identity change. The stale-cache tier keeps a fingerprint-agnostic last-known-good alias, so a previously-seen flow can still render offline even right after the identity changes.

## Enabling and disabling caching

Caching is on by default. Turn it off with `cachingEnabled: false`:

```tsx theme={null}
FlowPilot.configure({
  apiKey: 'fp_live_...',
  appId: 'your-app-id',
  cachingEnabled: false, // every present does a live resolve; no offline cache
});
```

Disabling caching also disables [prefetching](/expo-sdk/prefetching) (there is nothing to retain), so leave it on unless you have a specific reason not to.

## Clearing the cache

```ts theme={null}
// Clear cached flows (memory + disk). Forces the next resolve to re-fetch.
// Async (the disk delete); await it before the next resolve to avoid a stale hit.
await FlowPilot.clearCache();

// Clear image, icon, font, and bundled-image tracking.
FlowPilot.clearImageCache();
```

`clearCache()` is the lever to pull during testing when you have published a new flow version and want to bypass the TTL. It returns a promise (the disk delete is async), so `await` it before the next resolve, or a resolve fired immediately after can still read the old Tier-0 entry. In production, prefer attaching the new version in the dashboard and letting the TTL expire.

## Common mistakes

* **Testing caching against `development`.** The `0` default TTL means nothing stays fresh. Use `staging`/`production`.
* **Disabling caching and expecting prefetch to work.** Prefetch is a no-op with caching off.
* **Expecting a republished flow to appear instantly.** A fresh cached copy is served until its TTL expires, and the placement must point at the new version. Clear the cache or attach the new version. See [Troubleshooting](/expo-sdk/troubleshooting).
* **Assuming offline always works.** Offline rendering needs either a prior cache entry or a bundled default. Ship a [bundled default](/expo-sdk/offline-bundled-flows) for guaranteed offline onboarding.

## Troubleshooting

* **Old flow still showing after republish.** The cache is fresh, or the placement still points at the old version. Attach the new version, then `clearCache()` or wait out the TTL.
* **First present is slow on a poor network.** Expected on a cold cache. Ship a bundled default and prefetch at launch so Tier 0/Tier 3 cover the first present. See [Prefetching](/expo-sdk/prefetching).
* **A flow renders offline that you did not expect.** It came from stale cache or a bundled default. Check the `delivery_source` in debug logs.

## Related pages

* [Offline and bundled flows](/expo-sdk/offline-bundled-flows)
* [Prefetching and readiness](/expo-sdk/prefetching)
* [Media preloading](/expo-sdk/media-preloading)
* [Configuration](/expo-sdk/configuration)
* [Error handling](/expo-sdk/error-handling)
