●GA — Gemini 3.6 Flash reached general availability on July 21. Pricing is $1.50 input and $7.50 output per million tokens, down from $9.00 output on 3.5 Flash●TOKENS — On the Artificial Analysis Index, 3.6 Flash uses 17% fewer output tokens than 3.5 Flash. On agentic benchmarks like DeepSWE the average reportedly fell from 276K to 97K tokens per task●LITE — Gemini 3.5 Flash-Lite is also generally available at $0.30 / $2.50 per million tokens, aimed at low-latency, high-volume automation●OMNI — gemini-omni-flash-preview entered public preview. It generates 3 to 10 second videos at 720p and lets you refine them conversationally●SUNSET — Older image generation models shut down August 17, the Grok 4.1 family on the Gemini Enterprise Agent Platform on August 20, and gemini-robotics-er-1.6-preview on August 31●COMPUTER — Computer Use is in public preview for Gemini 3.5 Flash, covering browser, mobile, and desktop environments with configurable safety policies and prompt injection detection●GA — Gemini 3.6 Flash reached general availability on July 21. Pricing is $1.50 input and $7.50 output per million tokens, down from $9.00 output on 3.5 Flash●TOKENS — On the Artificial Analysis Index, 3.6 Flash uses 17% fewer output tokens than 3.5 Flash. On agentic benchmarks like DeepSWE the average reportedly fell from 276K to 97K tokens per task●LITE — Gemini 3.5 Flash-Lite is also generally available at $0.30 / $2.50 per million tokens, aimed at low-latency, high-volume automation●OMNI — gemini-omni-flash-preview entered public preview. It generates 3 to 10 second videos at 720p and lets you refine them conversationally●SUNSET — Older image generation models shut down August 17, the Grok 4.1 family on the Gemini Enterprise Agent Platform on August 20, and gemini-robotics-er-1.6-preview on August 31●COMPUTER — Computer Use is in public preview for Gemini 3.5 Flash, covering browser, mobile, and desktop environments with configurable safety policies and prompt injection detection
Measuring Update Policies for Memory Profiles: The Guard That Cost Me 16 Points of Accuracy
Memory profiles went GA in Memory Bank, making structured memory available to downstream code. I built three update policies and compared them under identical conditions. The one that looked obviously correct turned out to be the worst. Full harness code and the path to per-field TTLs.
I stopped scrolling halfway through a support log.
The profile for that user said billing_plan: pro, but the message in front of me said they were still on the free tier. The record had last been updated three weeks earlier. Three weeks ago they really had been on Pro, and the record came from something they had explicitly stated. High confidence, correctly stored.
Nothing about that value was wrong. It had simply gone stale while remaining true.
Memory profiles reached general availability in the August 1, 2026 changelog. Define a static schema, and the model fills and updates it — structured memory that downstream code can actually branch on, instead of free-form prose. I was genuinely pleased reading that. Then I started wiring it into an app and discovered the hard part was never the schema shape. It was deciding when to accept a write and when to refuse one.
I did not want to settle that by intuition, so I wrote a small harness and measured it. The results went the opposite direction from what I expected.
Structuring memory moves responsibility, not format
The difference between free-form memory and Memory profiles gets described as prose versus JSON. After running both, that framing misses the point.
Free-form memory can hold a contradiction indefinitely. Leave two lines side by side — "said Pro in early July, said free in late July" — and the reading model resolves it in context. It is verbose, but it lets you defer the decision.
A structured profile has nowhere to defer to. A billing_plan field holds exactly one value, which means the moment of writing is the moment of deciding. If you structure your memory without designing who makes that decision and how, the last write silently becomes the truth your downstream code branches on.
I underestimated this at first. My assumption was that a carefully cut schema would let the model handle the rest.
Putting confidence into the schema
So the first change was storing where each value came from, not just what it was.
confirmed means the user stated it outright; inferred means the model deduced it from context. With that distinction in place, a guard rule — never overwrite a confirmed value with an inferred one — should be enough to keep the profile honest. That was the hypothesis this article demolishes.
Measuring injected tokens first
Before touching policy, I wanted to know how much structuring buys you at all. I tokenized a 20-field profile against free-form memory holding the same twenty facts.
I used cl100k_base for counting. It is not Gemini's tokenizer, so read these as ratios rather than absolute values. The relative behavior on mixed Japanese and English text is still informative.
import json, tiktokenenc = tiktoken.get_encoding("cl100k_base")tok = lambda s: len(enc.encode(s))fields = { "primary_language": "Swift", "target_platform": "iOS", "billing_plan": "pro", "preferred_locale": "ja", "build_tool": "Xcode", "notify_channel": "push", "team_size": "1", "ci_provider": "Xcode Cloud", "min_os_version": "iOS 26", "analytics_tool": "App Store Connect", "paywall_style": "hard", "review_prompt_timing": "third launch", "asset_pipeline": "Figma to SwiftGen", "crash_tool": "Crashlytics", "localization_langs": "ja,en", "monetization": "subscription", "test_device": "iPhone 15", "design_system": "custom", "backend": "Cloudflare Workers", "push_provider": "APNs",}profile = json.dumps(fields, ensure_ascii=False, separators=(",", ":"))def freeform(history_depth): """Same 20 facts as prose, with history_depth prior revisions attached.""" out = [] for k, v in fields.items(): out.append(f"- The user's {k} is {v}.") for i in range(history_depth): out.append(f" (They stated a different value {i+1} revisions ago; the above is current.)") return "\n".join(out)print(f"profile: {tok(profile)} tok")for h in (0, 1, 2, 3): f = tok(freeform(h)) print(f"freeform(history={h}): {f} tok ratio={f / tok(profile):.1f}x")
Format
Tokens
vs profile
Structured profile (20 fields)
137
1.0x
Free-form, no history
296
2.2x
Free-form, 1 revision
896
6.5x
Free-form, 2 revisions
1,496
10.9x
Free-form, 3 revisions
2,096
15.3x
Against history-free prose the gap is only 2.2x. Unremarkable. The spread opens up once revision history accumulates.
At Gemini 3.6 Flash input pricing of $1.50 per Mtok, 1,000 requests a day for 30 days works out to $6.17 a month at 137 tokens versus $67.32 at 1,496 tokens — roughly $61 apart. At indie developer scale that is not noise.
But this is not an argument that structure wins. Free-form memory closes the gap if you compact it regularly. The real reason I moved to Memory profiles is that I no longer have to operate that compaction myself.
✦
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
✦A guard that refuses to overwrite confirmed values with inferred ones dropped accuracy from 68.9% to 52.5% on fast-changing fields — and why
✦Complete runnable comparison harness that lifted a mixed profile from 76.0% to 81.8% by switching to per-field TTLs
✦Token cost of structured profiles vs free-form memory: 137 vs 1,496 tokens, or $6.17 vs $67.32 per month at 1,000 requests a day
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.
Here is the actual experiment. Writes into the profile were governed by three policies:
naive — last write always wins
guard — never overwrite a confirmed value with an inferred one
guard + TTL — same guard, except a confirmed value older than N turns may be overwritten by an inferred one
Scoring is simple: at the end of the run, what fraction of fields matches the true current value? Ground truth mutates over time with a fixed per-turn probability. 25% of utterances carry confirmed values; the rest are inferred, and inferred values are wrong 20% of the time.
import randomVALUES = list("abcd")CONFIRM_P = 0.25 # probability an utterance is an explicit confirmed valueNOISE = 0.20 # probability an inferred value misses the truthdef run(policy, ttl, change_p, seed, n_keys=20, turns=2000): r = random.Random(seed) keys = [f"f{i:02d}" for i in range(n_keys)] truth = {k: r.choice(VALUES) for k in keys} # store[k] = [value, confirmed_flag, last_updated_turn] store = {k: [truth[k], 1, 0] for k in keys} for t in range(1, turns + 1): # the world moves on its own for k in keys: if r.random() < change_p: cur = truth[k] truth[k] = r.choice([v for v in VALUES if v != cur]) k = r.choice(keys) if r.random() < CONFIRM_P: v, confirmed = truth[k], 1 else: v = truth[k] if r.random() > NOISE else \ r.choice([x for x in VALUES if x != truth[k]]) confirmed = 0 cur = store[k] if policy == "naive": store[k] = [v, confirmed, t] elif policy == "guard": if not (cur[1] == 1 and confirmed == 0): store[k] = [v, confirmed, t] elif policy == "guard_ttl": stale = (t - cur[2]) > ttl if stale or not (cur[1] == 1 and confirmed == 0): store[k] = [v, confirmed, t] hit = sum(1 for k in keys if store[k][0] == truth[k]) return hit / len(keys)def avg(policy, ttl, change_p, trials=40): return sum(run(policy, ttl, change_p, s) for s in range(trials)) / trials * 100for change_p, label in ((0.001, "slow"), (0.004, "medium"), (0.016, "fast")): print(f"== change_p={change_p} ({label}) mean fact lifetime ~{1/change_p:.0f} turns ==") print(f" naive {avg('naive', None, change_p):.1f}% " f"guard {avg('guard', None, change_p):.1f}%", end="") for ttl in (50, 100, 200, 400): print(f" TTL{ttl}:{avg('guard_ttl', ttl, change_p):.1f}%", end="") print()
change_p is the per-turn probability that a field's underlying fact changes. At 0.001 the mean lifetime is about 1,000 turns; at 0.016 it is about 62. The former models slow attributes like primary language or target platform, the latter volatile ones like billing state or whatever the user is working on right now.
One honest caveat: this harness never calls the Gemini API. What it measures is the behavior of the merge layer I wrote, not the model's recall quality. It is still a far better basis for choosing a policy than intuition was.
The result: the protective move missed the most
Fact volatility
naive
guard
TTL 50
TTL 100
TTL 200
TTL 400
Slow (~1,000 turns)
83.7%
93.0%
90.4%
92.1%
93.0%
93.0%
Medium (~250 turns)
77.9%
78.5%
82.0%
81.8%
81.0%
78.6%
Fast (~62 turns)
68.9%
52.5%
66.5%
60.0%
54.8%
53.0%
The bottom row is where I sat back.
On fast-changing fields, the guard trails naive by 16.4 points — 68.9% down to 52.5%. Refusing to overwrite confirmed values, which reads as unambiguously correct, made accuracy measurably worse.
The mechanism is clear in hindsight. Once a confirmed value lands, it is frozen until the user states something explicitly again. With only 25% of utterances confirmed, that opportunity arrives every few dozen turns on average — while the underlying fact turns over every 62. The value you are protecting expires while you are protecting it.
That billing_plan: pro from the support log was exactly this. The record was correct, the guard was working, and the answer was still wrong.
TTL behavior was counterintuitive too. On fast fields, shorter is better: stretch the TTL to 400 and you land at 53.0%, barely distinguishable from the bare guard's 52.5%. Slow fields invert this — TTL 50 gives 90.4%, worse than the bare guard's 93.0%, because you discard good confirmed values too eagerly.
There is no safe default TTL. It has to track the lifetime of the fact the field holds.
Giving each field its own lifetime
Real profiles mix lifetimes in a single object. I rebuilt the test with 20 fields — 8 slow, 6 medium, 6 fast — and compared a single global TTL against per-field TTLs set to roughly a quarter of each field's mean lifetime (200 / 50 / 15 turns).
Policy
Accuracy
vs naive
naive (last-write-wins)
76.0%
—
guard only
76.0%
±0.0pt
guard + global TTL 50
79.5%
+3.5pt
guard + global TTL 100
79.3%
+3.3pt
guard + global TTL 200
77.3%
+1.3pt
guard + per-field TTL
81.8%
+5.8pt
Mixing lifetimes erases the guard's advantage entirely. Gains on slow fields and losses on fast ones cancel out, landing on the same 76.0% as naive. As long as one policy governs the whole profile, the average will not improve.
Per-field TTLs reach 81.8%, beating the best global setting by another 2.3 points. So I moved lifetime declarations into the schema itself.
FIELD_POLICY = { # slow attributes: trust an explicit statement for a long time "primary_language": {"ttl_turns": 200, "guard": True}, "target_platform": {"ttl_turns": 200, "guard": True}, "min_os_version": {"ttl_turns": 200, "guard": True}, # medium: revisit every few dozen turns "build_tool": {"ttl_turns": 50, "guard": True}, "ci_provider": {"ttl_turns": 50, "guard": True}, # volatile: do not over-protect "billing_plan": {"ttl_turns": 15, "guard": True}, "current_task": {"ttl_turns": 15, "guard": True}, "notify_channel": {"ttl_turns": 15, "guard": True},}DEFAULT_POLICY = {"ttl_turns": 50, "guard": True}def accept_update(field, current, incoming, now_turn): """Write to the profile only when this returns True.""" pol = FIELD_POLICY.get(field, DEFAULT_POLICY) if current is None: return True if not pol["guard"]: return True if incoming["confidence"] == "confirmed": return True # explicit statements always pass if current["confidence"] == "inferred": return True # inferred over inferred: newest wins # overwrite a confirmed value only once it has expired return (now_turn - current["updated_turn"]) > pol["ttl_turns"]
One trap worth flagging. It is tempting to express ttl_turns in wall-clock time — 30 days, say — but conversation frequency varies wildly per user, and wall-clock TTLs produce an inversion where your heaviest users hold the stalest values the longest. Turn counts matched observed behavior much better. In production I now carry both and expire on whichever threshold trips first.
What belongs in the schema, and what does not
Measuring changed my criteria for what to structure at all. Three rules I now apply:
1. Only values that downstream code reads mechanically
If code branches on it — billing state, locale, notification channel — it goes in the schema. Softer signals like "this person cares a lot about visual polish" stayed in free-form memory. If structuring a value does not enable a branch, all you have added is the cost of deciding.
2. Prefer closed value ranges
Anything expressible as an enum structures cleanly; free-text fields invite bad confirmed values. During development a device field once stored the literal string "probably some iPhone" as a confirmed value, and a downstream branch broke quietly for days. Fields that cannot be enumerated now get abstracted until they can be.
3. Allow blanks
This mattered most. Every field you mark required is a field the model dislikes leaving empty, so it fills it by inference — and downstream code cannot distinguish that from a confirmed value. I trimmed required to fields without which processing genuinely cannot proceed. The point of structuring memory is not to eliminate blanks, but to make a blank representable as a blank.
What you can change tomorrow
List the fields in your live profile and write next to each one how many days that fact typically survives. Some fields will resist a number. Those are your riskiest ones right now.
The harness above is about 50 lines. Drop in change_p values that match your own fields and run it — your numbers will differ from my 16.4 points, and that difference is the actual decision input.
Memory profiles gave us a place to keep memory. Deciding what to forget, and when, is still ours to do.
Availability and behavior of Memory profiles will keep shifting, so check the Gemini API changelog before you build against it.
Thank you for staying with a fairly long measurement write-up. I am still finding my footing here, so if your workload produces different results, I would love to hear about it.
Share
Thank You for Reading
Gemini 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.