●TOKENS — Gemini 3.6 Flash does the same work on roughly 17% fewer tokens than 3.5 Flash, with fewer unwanted code edits and shorter execution loops●CUTOFF — The knowledge cutoff moves forward from January 2025 to March 2026, which changes how questions about the last year land●BREAKING — On 3.6 Flash, custom temperature, top-K, and top-P values are ignored, and frequency or presence penalty values now return an API error●LITE — Gemini 3.5 Flash-Lite reaches general availability as the low-latency, low-cost option aimed at high-volume automation●ROBOTICS — The gemini-robotics-er-1.6-preview model shuts down on August 31, so anything still pinned to the preview needs a migration target●DOCS — Gemini writing and formatting tools began a gradual rollout to Scheduled Release domains in Docs on August 1, and Gemini Omni is now available inside Google Vids●TOKENS — Gemini 3.6 Flash does the same work on roughly 17% fewer tokens than 3.5 Flash, with fewer unwanted code edits and shorter execution loops●CUTOFF — The knowledge cutoff moves forward from January 2025 to March 2026, which changes how questions about the last year land●BREAKING — On 3.6 Flash, custom temperature, top-K, and top-P values are ignored, and frequency or presence penalty values now return an API error●LITE — Gemini 3.5 Flash-Lite reaches general availability as the low-latency, low-cost option aimed at high-volume automation●ROBOTICS — The gemini-robotics-er-1.6-preview model shuts down on August 31, so anything still pinned to the preview needs a migration target●DOCS — Gemini writing and formatting tools began a gradual rollout to Scheduled Release domains in Docs on August 1, and Gemini Omni is now available inside Google Vids
The Day the Knowledge Cutoff Moved Forward, the Stale Part Was My System Instruction
When a model's knowledge cutoff advances, the thing that goes stale is not the model — it is the dated assertions in your system instruction. Here is why only the lines written between the two cutoffs flip from helpful to contradictory, plus a working audit script and its measured results.
The day after I switched an image-classification pipeline over to Gemini 3.6 Flash, the output got sloppier. It is a small thing I run as an indie developer, which is exactly why nobody else was going to notice.
Categories came back coarser. Calls that used to reach the second level of the taxonomy now stopped at the first.
The model was supposed to be the newer one. Lower price per token, better token efficiency. Only the output quality had moved backwards.
The model was not the problem. One line I had written months earlier was still sitting in the system instruction:
As of November 2025, this API does not accept image and text input together.
Back in the 3.5 Flash era that line was a correct, useful supplement. I was filling in something the model genuinely did not know.
But 3.6 Flash carries a knowledge cutoff that moved from January 2025 to March 2026 (check the official release notes for the current values). The model already knows that particular constraint changed.
A supplement had quietly turned into a contradiction.
The model is not what goes stale — your prompt is
Discussions of knowledge cutoffs almost always run in one direction: the model does not know about recent things.
In practice, the pain arrives from the other side first.
A dated assertion in a system instruction is frozen the moment you type it. The model's knowledge keeps advancing; the instruction does not. Every cutoff bump widens the gap.
And it fails silently. No API error. Structured-output validation still passes. The model simply has to decide whether to trust its own knowledge or your instruction, and it hedges.
In my case that hedging surfaced as reduced category depth. A model holding a contradiction leans away from committing.
The danger zone closes around the middle band
That does not mean you should review every dated line in your prompt.
When a line was written splits it into three very different cases.
When it was written
Role under 3.5 Flash
Meaning under 3.6 Flash
Action
Before the old cutoff (Jan 2025)
Duplicated something the model already knew
Still duplicated. Costs tokens, adds nothing
Deletion candidate, low priority
Between the old and new cutoffs
Genuinely filled a blind spot
Now competes with the model's own knowledge
Review and rewrite first
After the new cutoff (Mar 2026)
(did not exist yet)
Still a necessary supplement
Keep
Only the middle band reverses its role.
The lines that were once the most valuable are now the ones getting in the way. That asymmetry is what caught me off guard.
The band is also wide: January 2025 to March 2026 is fourteen months, which for many of us overlaps almost exactly with the period we ran 3.5 Flash in production. The more diligently you documented the model's blind spots back then, the more reversed lines you are carrying now.
✦
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 three-way classifier — before the old cutoff, between the two, after the new one — showing why the danger zone closes around the middle band only
✦An audit script that extracts dated assertions from system instructions (v1 caught 5 of 7, v2 caught all 7, with zero false positives across a 6-line negative set)
✦A measured comparison against the obvious 'review anything older than 12 months' approach, which overlapped by only 0.40 Jaccard
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.
System instructions get long enough that manual review stops being realistic. The logic is simple enough to script.
Two jobs: pull dated assertions out of the instruction text, then compare each date against the two cutoffs.
#!/usr/bin/env python3"""cutoff_window_audit.py — classify dated assertions against two model cutoffs."""from __future__ import annotationsimport argparseimport jsonimport reimport sysfrom dataclasses import dataclass, asdictfrom datetime import datefrom pathlib import PathMONTH_NAME = ( r"(?:jan(?:uary)?|feb(?:ruary)?|mar(?:ch)?|apr(?:il)?|may|jun(?:e)?|" r"jul(?:y)?|aug(?:ust)?|sep(?:t(?:ember)?)?|oct(?:ober)?|nov(?:ember)?|dec(?:ember)?)")EN_DATE = re.compile(rf"\b(?P<mon>{MONTH_NAME})\.?,?\s+(?P<y>19\d{{2}}|20\d{{2}})\b", re.I)ISO_DATE = re.compile(r"\b(?P<y>20\d{2})-(?P<m>0[1-9]|1[0-2])(?:-\d{2})?\b")# Leading "As of <date>," / "2026-01:" framing, stripped before judging the clauseDATE_FRAMING = re.compile( rf"^\s*(?:as\s+of\s+)?(?:{MONTH_NAME}\.?,?\s+\d{{4}}|20\d{{2}}-\d{{2}}(?:-\d{{2}})?)\s*[,:—-]?\s*", re.I,)MONTHS = { "jan": 1, "feb": 2, "mar": 3, "apr": 4, "may": 5, "jun": 6, "jul": 7, "aug": 8, "sep": 9, "oct": 10, "nov": 11, "dec": 12,}ASSERTIVE = ( "not supported", "does not support", "does not exist", "unavailable", "is not available", "cannot be used", "no support for", "latest",)HEDGED = ("may ", "might ", "possibly", "e.g.", "for example", "such as")# Second pass: a dated clause carrying a copula is a fact claim, not an instructionCOPULA = re.compile(r"\b(?:is|are|was|were|has|have|had|does|do|remains?|will)\b", re.I)@dataclassclass Assertion: source: str line_no: int text: str asserted_on: str | None verdict: str reason: strdef _to_month(y: int, m: int | None) -> date: return date(y, m or 1, 1)def _without_dates(line: str) -> str: """Blank out date literals so month names cannot collide with hedge words.""" return ISO_DATE.sub(" ", EN_DATE.sub(" ", line))def extract_date(line: str) -> date | None: """Return the earliest month-precision date pinned in the line, if any.""" found: list[date] = [] for hit in EN_DATE.finditer(line): found.append(_to_month(int(hit.group("y")), MONTHS[hit.group("mon")[:3].lower()])) for hit in ISO_DATE.finditer(line): try: found.append(_to_month(int(hit.group("y")), int(hit.group("m")))) except ValueError: continue return min(found) if found else Nonedef is_assertive(line: str, dated: bool = False) -> bool: clause = DATE_FRAMING.sub("", line) low = _without_dates(clause).lower() if any(h in low for h in HEDGED): return False if any(tok in low for tok in ASSERTIVE): return True return bool(dated and COPULA.search(clause))def classify(pinned: date | None, old_cutoff: date, new_cutoff: date) -> tuple[str, str]: if pinned is None: return "undated", "no date found; needs human review" if pinned < old_cutoff: return "redundant", "predates the old cutoff; the model already knew this" if pinned < new_cutoff: return "contested", "between the cutoffs; a supplement that flipped into a contradiction" return "current", "after the new cutoff; still needed"def audit(paths: list[Path], old_cutoff: date, new_cutoff: date) -> list[Assertion]: out: list[Assertion] = [] for path in paths: try: body = path.read_text(encoding="utf-8") except (OSError, UnicodeDecodeError) as exc: print(f"skip {path}: {exc}", file=sys.stderr) continue for i, line in enumerate(body.splitlines(), start=1): stripped = line.strip() if not stripped or stripped.startswith("#"): continue pinned = extract_date(stripped) if not is_assertive(stripped, dated=pinned is not None): continue verdict, reason = classify(pinned, old_cutoff, new_cutoff) out.append( Assertion( source=path.name, line_no=i, text=stripped[:120], asserted_on=pinned.isoformat() if pinned else None, verdict=verdict, reason=reason, ) ) return outdef parse_month(text: str) -> date: y, m = text.split("-")[:2] return date(int(y), int(m), 1)def main() -> int: ap = argparse.ArgumentParser() ap.add_argument("paths", nargs="+", type=Path) ap.add_argument("--old-cutoff", required=True, help="e.g. 2025-01") ap.add_argument("--new-cutoff", required=True, help="e.g. 2026-03") ap.add_argument("--json", action="store_true") ap.add_argument("--fail-on-contested", action="store_true") args = ap.parse_args() old_cutoff, new_cutoff = parse_month(args.old_cutoff), parse_month(args.new_cutoff) if old_cutoff >= new_cutoff: ap.error("--old-cutoff must precede --new-cutoff") files = [p for p in args.paths if p.is_file()] rows = audit(files, old_cutoff, new_cutoff) if args.json: print(json.dumps([asdict(r) for r in rows], ensure_ascii=False, indent=2)) else: for r in rows: print(f"[{r.verdict:9}] {r.source}:{r.line_no} ({r.asserted_on or 'undated'}) {r.text}") counts: dict[str, int] = {} for r in rows: counts[r.verdict] = counts.get(r.verdict, 0) + 1 print("---", " ".join(f"{k}={v}" for k, v in sorted(counts.items())) or "no findings") contested = sum(1 for r in rows if r.verdict == "contested") return 1 if (args.fail_on_contested and contested) else 0if __name__ == "__main__": raise SystemExit(main())
The classification itself is three branches. The hard part turned out to be the step before it — deciding which lines are factual claims at all, and English made that harder than Japanese did.
What the first version missed
I ran the audit against an eleven-line instruction close to what I had actually been shipping.
# Image classification assistant — system instruction (excerpt, under audit)As of December 2024, return Japanese category names verbatim without normalising them.As of May 2023, structured output is not supported, so embed the JSON in the body text.As of June 2025, thinking_budget is unavailable on the Flash tier.As of November 2025, this API does not accept image and text input together.2026-01: the Files API upload retention window is 48 hours.As of April 2026, the Batch API concurrency limit is per project.2026-05: this feature is not supported.Always answer in Japanese, using polite forms.When uncertain, you may return "undetermined".As of August 2025, the latest alias updates automatically.Responses may run long. That was the behaviour as of March 2025.
The first implementation relied on the ASSERTIVE vocabulary alone. It found five findings against a ground truth of seven.
The two it skipped were these:
2026-01: the Files API upload retention window is 48 hours.As of April 2026, the Batch API concurrency limit is per project.
Neither contains a negation. They just state a value.
From a staleness perspective that shape is the most dangerous of all. Numbers and enumerations change more quietly than capability claims, and nothing in the sentence signals that it has drifted.
So I added a second pass: strip the leading date framing, then treat what remains as a fact claim if it carries a copula. As of December 2024, return the names verbatim loses its framing and reveals a bare imperative with no copula, so it stays out. 2026-01: the retention window is 48 hours keeps its is and comes in.
Then English handed me a bug I did not see coming.
As of May 2023, structured output is not supported vanished from the results. The hedge list contains "may ", and the month name May matched it. A perfectly good assertion was being suppressed by a calendar collision.
The fix is to blank out date literals before running the hedge check:
def _without_dates(line: str) -> str: """Blank out date literals so month names cannot collide with hedge words.""" return ISO_DATE.sub(" ", EN_DATE.sub(" ", line))
Obvious in hindsight. It would never have surfaced in the Japanese version, where the month is written 5月 and collides with nothing.
With both fixes in place, the second version caught all seven.
I measured the false-positive side too. Against a six-line negative set — business instructions, date strings meant to be copied through, a fixed test date — both versions produced zero findings. Without the framing-strip and copula test, four of those six lines would have been flagged.
One line is skipped deliberately: Responses may run long. That was the behaviour as of March 2025. Here the may is a genuine hedge, and a claim that does not commit does not collide head-on with the model's knowledge. Reasonable people will disagree; empty out HEDGED if you want it caught.
Throughput never became a concern. Across 500 files and 9,500 lines the audit ran in a median of 0.205 seconds — roughly 0.41 ms per file, about 46,000 lines per second (Python 3.10.12, x86_64, 4-core container, median of five runs). It disappears inside a CI step.
"Review anything older than 12 months" catches less than half
Once the tool worked, an obvious question surfaced. Why bother with cutoff dates at all? Why not simply review anything old?
I ran both criteria against the same eleven lines: the cutoff-window verdict (contested) versus "written more than 12 months ago".
Criterion
Lines flagged
Count
Cutoff window (contested)
4, 5, 6, 11
4
Older than 12 months
3, 4, 11
3
Overlap
4, 11
2
Jaccard similarity: 0.40.
The breakdown is the interesting part. The age-based criterion flags line 3 (May 2023), which sits before the old cutoff — harmless, merely wasting tokens. And it misses lines 5 (November 2025) and 6 (January 2026), which are precisely where I actually got burned.
The reason is that elapsed time is measured from today, while the breakage is anchored to the point where the model's cutoff moved. Different axis, different set.
Age and risk are not proportional. That alone convinced me to keep the audit anchored to cutoff dates.
Wiring it into CI
Because the logic is small, gating is cheap. --fail-on-contested exits non-zero when any contested line survives.
Running it for real surfaced a few adjustments. Next time a cutoff moves, this is the order I plan to work through.
Keep the cutoff values out of the script. Passing them as arguments means the next bump is a matter of promoting the new value into the old slot. The classifier itself never changes.
Mark deliberate exceptions. Internal rules the model cannot possibly know need to stay, dates and all. I tag those lines with a trailing # pinned:intentional, keeping the justification on the same line — mostly so that future me has something to read.
Route undated to a warning, not a failure. The tool cannot classify an assertion with no date, and failing the build breaks every time someone forgets a date mid-development.
Treat it as a contract test. Define "a contested line survives" as broken, and the audit stops sliding down the backlog.
How strict to be depends on how often you touch the prompt. For teams editing instructions daily, I would recommend starting in warning-only mode. If your system instructions move only a couple of times a year, turn on --fail-on-contested from day one — in that case the cutoff bump is the only review opportunity you get.
There is a family resemblance here to fixture freshness. I wrote earlier about gating on recorded fixtures and model retirement dates; that watches an external deadline, while this watches the expiry date on claims you wrote yourself. Different subject, same instinct.
Better still: stop writing dated claims
Auditing is a treatment, not a cure. The cure is not putting time-dependent facts into system instructions at all.
That is not always possible. Internal specifications, changes too recent to be in training data — those supplements will keep being necessary.
What I changed was the phrasing.
# BeforeAs of November 2025, this API does not accept image and text input together.# AfterIf your knowledge about combined image and text input disagrees with theactual behaviour, trust the error message you actually received.
Instead of pinning a fact, pin a priority. When the cutoff advances and the model's own knowledge becomes correct, the instruction retires itself rather than fighting back.
Not every supplement converts this cleanly. But each line you convert is one less line to audit the next time a cutoff moves.
Compared with this, the sampling-parameter deprecations feel almost considerate — at least those announce themselves with an error (I wrote about that in the deprecation that hurt diversity before determinism). An advancing knowledge cutoff sends no signal at all.
Wrapping up
When a model's cutoff moves, start with the assertions written between the old and new cutoff dates. That band is the only place where a supplement flips into a contradiction.
If you want to use the script as-is, pass the previous model's cutoff as --old-cutoff and the new model's as --new-cutoff. That single run is enough to tell you where to look.
For what it's worth, I only built this after spending three days chasing why the output had gone vague. I wish I had measured sooner — and I hope this saves someone else the detour. Thank you for reading.
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.