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).cachingEnabled(defaulttrue) 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(defaultnil) overrides where the disk cache lives.niluses a platform default location.
FlowPilotConfiguration.
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 and 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. |
.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 acache_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 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 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, 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.
clearCache() and clearImageCache() on logout to drop the previous user’s data entirely (see 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:
FlowPilotConfiguration. At configure time the SDK copies these onto the shared image cache:
imageMemoryCacheSize(default50 * 1024 * 1024, 50 MB) setsmaxMemoryCacheSize.imageDiskCacheSize(default200 * 1024 * 1024, 200 MB) setsmaxDiskCacheSize.
ImageCache type also exposes, for advanced use:
In-memory budget in bytes. Mutable at runtime.
On-disk budget in bytes. Mutable at runtime.
How long a downloaded image stays valid before it is treated as expired.
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 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()isasyncand clears cached flows, registered fonts, and icons.clearImageCache()clears the image cache and the icon cache.
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:Clear everything on logout
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. - 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.
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()andclearImageCache()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: falsethere 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, or prefetch(_:warmMedia: true) / 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 |