> ## 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 SDK caches resolved flows and downloaded images so repeat presentations are instant and survive a flaky network.

FlowPilot caches two things: the resolved flow (the JSON the backend returns for a placement) and the images that flow downloads. Caching makes a repeat present instant, and it is the backbone of the offline fallback chain, so a present never hangs waiting on a bad network.

## How the flow cache works

The flow cache is on by default. It has a memory layer and a disk layer, both keyed by placement and by the identity the flow was resolved for (see [Identity-aware caching](#identity-aware-caching)).

* **`cachingEnabled`** (default `true`) turns the flow cache on or off. With it off, every present does a live resolve and there is no offline fallback to a previously resolved flow.
* **`cacheDirectory`** (default `nil`) overrides where the disk cache lives. `nil` uses a platform default location.

Both are set once in [`FlowPilotConfiguration`](/ios-sdk/configuration).

### Where the cache sits in the fallback chain

When you present a placement, the SDK walks a fixed order and stops at the first tier that produces a presentable flow. This is the same chain documented in [Offline and bundled flows](/ios-sdk/offline-bundled-flows) and [Error handling](/ios-sdk/error-handling):

| Tier | Source                   | When it is used                                                                                                                 |
| ---- | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------- |
| 0    | **Fresh cache**          | A non-expired cached flow exists. Rendered instantly, no network.                                                               |
| 1    | **Live network resolve** | Fetched from the API within `resolveTimeout`. The result refreshes the cache for next time.                                     |
| 2    | **Stale cache**          | The live resolve failed or timed out. The last successfully resolved flow is served, even past its freshness window.            |
| 3    | **Bundled default**      | No cache at all. A flow you shipped in the app bundle renders. See [Offline and bundled flows](/ios-sdk/offline-bundled-flows). |

Tiers 4 (your own native fallback) and 5 (a graceful `.error`) are handled by the calling API, not the cache.

The important detail: a cached flow whose freshness window has lapsed is **not deleted**. It stays on disk so Tier 2 can still serve it as the last known good experience when the network is down.

### Freshness (TTL)

How long a cached flow counts as "fresh" is decided by the backend, not the SDK. The resolve response carries a `cache_ttl_seconds` value, and the SDK uses it as the cache TTL. When the backend does not send one, the SDK defaults to **300 seconds (5 minutes)**. See [`cache_ttl_seconds`](/reference/sdk-rest-api) in the REST reference.

This is why a republished flow may not appear immediately. Propagation is bounded by that TTL plus the SDK's own cache. See [Rolling out an update](/editor/publishing) for the full picture.

### Identity-aware caching

A resolved flow is personalized: the backend picks a flow (and an experiment variant) from the user identity and the targeting attributes you send. The cache key reflects that, so a flow cached for one identity is never served to another.

In practice this means:

* After you change the user (for example on login) or update targeting attributes with [`updateContext`](/ios-sdk/variables-and-context), the next present for an affected placement resolves fresh (Tier 1) instead of reusing the previously cached flow. A flow cached while the user was anonymous is never shown to the now-identified user, and a stale A/B variant is never served after the inputs that picked it changed.
* The stale-cache fallback (Tier 2) still works across an identity change: when the network is down and there is no fresh match for the new identity, the SDK serves the last known good flow for the placement so the user still sees something.

You do not configure this; it is automatic. You should still call `clearCache()` and `clearImageCache()` on logout to drop the previous user's data entirely (see [Clearing caches](#clearing-caches)).

## How the image cache works

Images referenced by a flow are downloaded once and cached in memory and on disk, keyed by their full URL. The cache is a shared singleton, `ImageCache.shared`, reachable from the SDK:

```swift theme={null}
let cache = FlowPilot.shared?.imageCache
```

You set its sizes in `FlowPilotConfiguration`. At configure time the SDK copies these onto the shared image cache:

* **`imageMemoryCacheSize`** (default `50 * 1024 * 1024`, 50 MB) sets `maxMemoryCacheSize`.
* **`imageDiskCacheSize`** (default `200 * 1024 * 1024`, 200 MB) sets `maxDiskCacheSize`.

The `ImageCache` type also exposes, for advanced use:

<ParamField path="maxMemoryCacheSize" type="Int">
  In-memory budget in bytes. Mutable at runtime.
</ParamField>

<ParamField path="maxDiskCacheSize" type="Int">
  On-disk budget in bytes. Mutable at runtime.
</ParamField>

<ParamField path="defaultTTL" type="TimeInterval" default="7 days">
  How long a downloaded image stays valid before it is treated as expired.
</ParamField>

Public methods include `getImage(for:)`, `hasImage(for:)`, `setImage(_:for:ttl:)`, `setImageData(_:for:ttl:)`, `removeImage(for:)`, `clearAll()`, and `cleanExpired()`. Most apps never call these directly; the renderer and the [media preloader](/ios-sdk/media-preloading) use them for you.

## Clearing caches

Two methods clear caches. Call them when a user logs out or switches account, so one user never sees another user's cached flow or images.

* **`clearCache()`** is `async` and clears cached flows, registered fonts, and icons.
* **`clearImageCache()`** clears the image cache and the icon cache.

```swift theme={null}
// On logout / account switch.
Task {
    await FlowPilot.shared?.clearCache()
    FlowPilot.shared?.clearImageCache()
}
```

## Example

### Raise the image budget for an image-heavy flow

A flow that shows many large photos benefits from a bigger image cache so screens do not re-download as the user navigates back and forth. Set the sizes at launch:

```swift theme={null}
import FlowPilotSDK

FlowPilot.configure(
    FlowPilotConfiguration(
        apiKey: "fp_live_xxxxxxxxxxxxxxxx",
        appId: "your-app-id",
        imageMemoryCacheSize: 100 * 1024 * 1024,  // 100 MB in memory
        imageDiskCacheSize: 500 * 1024 * 1024      // 500 MB on disk
    )
)
```

You can also adjust the live cache after configuring:

```swift theme={null}
FlowPilot.shared?.imageCache.maxMemoryCacheSize = 100 * 1024 * 1024
```

### Clear everything on logout

```swift theme={null}
func handleLogout() {
    Task {
        await FlowPilot.shared?.clearCache()   // flows + fonts + icons
        FlowPilot.shared?.clearImageCache()    // images + icons
    }
    // ... your own sign-out work
}
```

## Notes

* **Caching is why a republished flow may not appear instantly.** A device may serve a fresh cached copy until its TTL lapses, then a new resolve picks up the change. Propagation is bounded by `cache_ttl_seconds`. See [Publishing](/editor/publishing).
* **A lapsed cache is still useful.** It powers the stale-cache tier (Tier 2), so the last good flow renders offline instead of failing.
* **Flows served from a degraded tier are tagged.** Every automatic event carries a `delivery_source` (`network`, `cache`, `stale_cache`, `bundled_default`), so you can tell offline and fallback renders apart in the dashboard. See [Analytics integration](/ios-sdk/analytics).

## Common mistakes

* **Expecting an instant rollout.** Publishing a new flow version does not invalidate caches on devices. A present can serve the cached flow until its TTL lapses. Plan rollouts around the TTL, not around the moment you click Publish.
* **Never clearing on user switch.** If your app supports multiple accounts, call `clearCache()` and `clearImageCache()` on logout. Otherwise the next user can see the previous user's cached flow and images.
* **Setting tiny cache sizes.** A memory or disk budget that is too small for your flow makes the cache thrash (evict and re-download constantly), which is slower than no tuning at all. Size the budget to your largest flow's media.
* **Turning caching off in production.** With `cachingEnabled: false` there is no fresh-cache tier and no stale-cache fallback, so every present needs a working network. Leave it on unless you have a specific reason.

## Troubleshooting

| Symptom                              | Likely cause                                                  | Fix                                                                                                                                                           |
| ------------------------------------ | ------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| A published change is not showing    | A fresh cached flow is still within its TTL                   | Wait out `cache_ttl_seconds`, or test on a device that has not cached the placement                                                                           |
| Images flash a placeholder then load | Cache miss on first present (nothing preloaded)               | Use [media preloading](/ios-sdk/media-preloading), or [`prefetch(_:warmMedia: true)`](/ios-sdk/prefetching) / `prefetchOnLaunch` to warm images ahead of time |
| One user sees another user's flow    | Cached images or stale-cache fallback from a previous account | Identity-aware keying prevents serving one identity's fresh flow to another, but call `clearCache()` and `clearImageCache()` on logout to drop the rest       |
| Memory spikes on image-heavy flows   | Memory budget too high for the device                         | Lower `imageMemoryCacheSize`; disk cache absorbs the rest                                                                                                     |

## Related pages

* [Offline and bundled flows](/ios-sdk/offline-bundled-flows)
* [Media preloading](/ios-sdk/media-preloading)
* [Prefetching and readiness](/ios-sdk/prefetching)
* [Configuration](/ios-sdk/configuration)
* [Publishing](/editor/publishing)
* [SDK REST API](/reference/sdk-rest-api)
