●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SONNET 4.5 — Retires on the Claude API on 11/30, 34 days left. If your app calls it, check the migration target●SDK 58 ISSUE — Early reports include Android widgets not refreshing on a schedule and Stripe linking statically●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SONNET 4.5 — Retires on the Claude API on 11/30, 34 days left. If your app calls it, check the migration target●SDK 58 ISSUE — Early reports include Android widgets not refreshing on a schedule and Stripe linking statically●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…
Remembering What the AI Decided: Four Marks in the Cache Key Paid Off on Model-Swap Day
A small on-device cache for AI answers, with the model name and prompt version in the key so old answers vanish on swap day. TypeScript plus simulated results.
I was scrolling a wallpaper list slowly one evening. Every time I scrolled back to a picture I had just passed, another classification request went out. Nothing changed on screen. Only the request count, and eventually the bill, changed.
As an indie developer, I use an image-reading AI to sort wallpapers into categories. A few people have asked how to do the same inside an app, so I wrote down the layer I would put in a Rork-built screen: one that remembers answers. The hard part turned out not to be remembering. It was deciding when to forget.
Below is the TypeScript for a small cache layer, exactly as I ran it, followed by results from a 400-image simulated catalog on my machine's Node. These are simulated runs with a fixed random seed, not measurements on a phone, and I want to say that up front.
What to remember, and what not to
Remember answers that can safely stay the same for the same input. A wallpaper category fits: if the image does not change, neither does the answer. A conversational reply does not, because it is allowed to differ each time.
Decide what not to remember, too. If the AI reports low confidence, do not store the answer. A stored low-confidence answer freezes that vagueness in place, and the image keeps the same fuzzy label every time you see it.
Situation
Store it?
Why
Classification with confidence 0.9
Yes
Same image, same answer is fine
Classification with confidence 0.4
No
Storing it would freeze the vagueness
Network failure or timeout
No
A stored failure keeps coming back after the network recovers
Right after swapping the model or prompt
Ignore old versions
Old answers would pass as new ones
Four marks go in the key
The image ID alone is not enough for a cache key. Put these four side by side.
Mark
Example
What happens without it
task
wallpaper-category
Answers for different jobs get mixed up
model
claude-sonnet-5-5
After a model change, the old model's answers keep coming back
promptVersion
p3
Edited instructions still get answers from before the edit
contentId
the image ID
You cannot tell which image an answer belongs to
You can see why the model belongs in the key by trying the naive version. I keyed on the image ID alone with a 30-day expiry, swapped the model, and counted the next 1,000 views. All 1,000 were answers from the old model. An expiry is a way to throw away what has become old; it is not a way to throw away what has been replaced.
For context on timing, Anthropic deprecated Claude Sonnet 4.5 (claude-sonnet-4-5-20250929) on September 30, 2026, with API retirement tentatively set for November 30. The recommended replacement is claude-sonnet-5-5. I wrote about finding model IDs hard-coded in an app in Find out whether your Rork app still calls Sonnet 4.5, before November 30. The layer below is where you land safely once you have found them.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦You can now copy a small layer that remembers the AI's answers on the device, and cut repeat calls for the same image to almost zero on the second launch
✦You will be able to stop old answers from lingering on screen after you swap a model or a prompt, before you ship
✦You can now spot, before writing it, the bug where one failed request makes every retry fail too
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
Storage is reduced to a minimal interface, KV, shaped like MMKV. In tests a Map stands in. The layer solves three problems: fewer repeat calls for the same image, one call when identical requests arrive at once, and a clean-up of old versions on swap day.
The value also carries model and promptVersion. Even when the key matches, a mismatched value is discarded. It is insurance in case someone changes how the key string is built later.
finally always removes the in-flight record. The next section measures what happens if you forget.
Answers below the confidence threshold are not stored. The price is that such an image triggers a call on every view. I count that cost below.
Wiring it into a screen
On the screen side, wrap MMKV in the KV shape and pass it in. MMKV method names vary by version (delete versus remove), so adjust only the wrapper to match the types in your version. For choosing MMKV and migrating to it, see Three weeks moving six wallpaper apps to MMKV.
// wallpaperCategory.ts — usage from the screen (the MMKV wrapper and hook are a type-checked sketch; I have not run them on a device)import { MMKV } from "react-native-mmkv";import { createAiCache, type KV } from "./aiCache";const mmkv = new MMKV({ id: "ai-answers" });const kv: KV = { getString: (k) => mmkv.getString(k), set: (k, v) => mmkv.set(k, v), delete: (k) => mmkv.delete(k), // `remove` in some versions getAllKeys: () => mmkv.getAllKeys(),};// On swap day, these two lines are the only thing you changeconst MODEL = "claude-sonnet-5-5";const PROMPT_VERSION = "p1";export const categoryCache = createAiCache( kv, { task: "wallpaper-category", model: MODEL, promptVersion: PROMPT_VERSION, ttlMs: 30 * 24 * 3600 * 1000, maxEntries: 2000, minConfidence: 0.6 }, async (id) => { // Point this at your own gateway, such as an Edge Function, so no key ships in the app. const res = await fetch(`https://YOUR_PROJECT.functions.supabase.co/classify?id=${id}`); if (!res.ok) throw new Error(`classify ${res.status}`); return (await res.json()) as { label: string; confidence: number }; },);// Once at startup: sweep answers left over from older models or prompts.categoryCache.sweepOldVersions();
The setup: a 400-image catalog viewed 3,000 times per launch, across two launches, with a seeded random generator that favors popular images. The AI call is a stub that returns confidence 0.4 about 8% of the time and 0.9 otherwise. I ran it on Node 22.
What I checked
No cache / naive version
This implementation
Calls in the first launch (3,000 views)
3,000
426
Calls in the second launch (3,000 views)
3,000
4
Old answers shown in the 1,000 views right after a model swap
1,000
0 (363 old entries swept, 356 fresh calls)
Calls for 20 simultaneous requests for one image
20
1
One failure followed by three retries
fail, fail, fail (1 call)
fail, ok, ok (2 calls)
The surprise was the 4 calls in the second launch. All four were low-confidence images. An image I decided not to store gets asked again on every launch. Of the 426 calls in the first launch, 30 were low-confidence answers that were not stored, and those 30 keep generating calls indefinitely.
The cost of swap day is worth stating plainly too. Not using old answers means asking again as images come up. In the simulation, the next 1,000 views triggered 356 calls. On the day you swap, that many extra calls will happen once.
The 400 stored entries came to about 60 KB, roughly 150 bytes each. Once the cache hit its 2,000-entry cap, each write cost about 0.65 ms in Node. A phone will differ, so treat it as a rough guide.
The stuck-failure bug reproduces with this short test. In the version that forgets to remove the in-flight record, the pending promise stays rejected and every retry receives the same failure.
// test_inflight.ts — node --experimental-strip-types test_inflight.tsimport { createAiCache } from "./aiCache.ts";import { memKV } from "./mem.ts";const cfg = { task: "t", model: "m1", promptVersion: "p1", ttlMs: 864e5, maxEntries: 100, minConfidence: 0.6 };// Forgetful version (removes only inside then)const inflight = new Map<string, Promise<number>>();let calls = 0;const flaky = async () => { calls++; if (calls === 1) throw new Error("timeout"); return 1; };const buggy = (k: string) => { const r = inflight.get(k); if (r) return r; const p = flaky().then((v) => { inflight.delete(k); return v; }); // not removed on failure inflight.set(k, p); return p;};const a: string[] = [];for (let i = 0; i < 3; i++) { try { await buggy("k"); a.push("ok"); } catch { a.push("fail"); } }console.log("forgetful:", a.join(","), "calls", calls); // fail,fail,fail calls 1// This implementationlet fail = true, n = 0;const cache = createAiCache(memKV(), cfg, async () => { n++; if (fail) { fail = false; throw new Error("timeout"); } return { label: "x", confidence: 0.9 }; });const b: string[] = [];for (let i = 0; i < 3; i++) { try { await cache.classify("1"); b.push("ok"); } catch { b.push("fail"); } }console.log("this one:", b.join(","), "calls", n); // fail,ok,ok calls 2
Pitfalls
Pitfall
What happens
What to do first
Keying on the image ID and relying on expiry for freshness
After a model swap, old answers return until they expire
Put model and promptVersion in both key and value
Removing the in-flight record only on success
One failure sticks, and retries never recover
Remove it in `finally`
Storing low-confidence answers
Vague answers get frozen and are hard to notice
Skip them, and count the cost of asking again
Forgetting to sweep on swap day
Old-version answers sit on the device using space
Call `sweepOldVersions` at startup
Editing the prompt without bumping promptVersion
Answers from before and after the edit get mixed
Make it a rule: touch the prompt, bump the version
Next step
The next time you swap a model ID, change just MODEL and PROMPT_VERSION, and count the calls made right after launch once. That count is your estimate of the extra cost on swap day. If it feels heavy, you can re-ask in order of the most-viewed images first.
Swapping looked like renaming a model, but it was really about deciding what to do with the answers already sitting on the device. When you let a device remember an answer, write down when it should forget, in the same place—I intend to hold to that line in whatever I build next.
Share
Thank You for Reading
Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.