Skip to main content
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).
  • 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.

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:
TierSourceWhen it is used
0Fresh cacheA non-expired cached flow exists. Rendered instantly, no network.
1Live network resolveFetched from the API within resolveTimeout. The result refreshes the cache for next time.
2Stale cacheThe live resolve failed or timed out. The last successfully resolved flow is served, even past its freshness window.
3Bundled defaultNo cache at all. A flow you shipped in the app bundle renders. See Offline and 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 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.
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).

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:
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:
maxMemoryCacheSize
Int
In-memory budget in bytes. Mutable at runtime.
maxDiskCacheSize
Int
On-disk budget in bytes. Mutable at runtime.
defaultTTL
TimeInterval
default:"7 days"
How long a downloaded image stays valid before it is treated as expired.
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 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.
// 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:
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:
FlowPilot.shared?.imageCache.maxMemoryCacheSize = 100 * 1024 * 1024

Clear everything on logout

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.
  • 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() 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

SymptomLikely causeFix
A published change is not showingA fresh cached flow is still within its TTLWait out cache_ttl_seconds, or test on a device that has not cached the placement
Images flash a placeholder then loadCache 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 flowCached images or stale-cache fallback from a previous accountIdentity-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 flowsMemory budget too high for the deviceLower imageMemoryCacheSize; disk cache absorbs the rest