●DEADLINE — gemini-robotics-er-1.6-preview shut down yesterday, August 31. If code started erroring today, this retirement is the first thing to check●NEXT — The next deadline is September 30, when the gemini-omni-flash-preview endpoint is retired. The migration target is gemini-omni-1.1-flash, GA since August 27●APIKEY — The Gemini API begins rejecting requests from standard API keys during September. If you have not moved to auth keys yet, this month is the real deadline●SHEETS — Sheets canvas, a Gemini-powered feature, began a gradual rollout to Scheduled Release domains on August 31, with up to 15 days before it becomes visible●MEET — Starting August 31, Google Meet hardware touch controllers can start, stop, and manage the Take notes for me feature directly from the in-room screen●PARAMS — temperature, top_p, and top_k remain silently deprecated: requests are accepted and the values ignored. Code that assumes determinism needs to be verified by measurement●DEADLINE — gemini-robotics-er-1.6-preview shut down yesterday, August 31. If code started erroring today, this retirement is the first thing to check●NEXT — The next deadline is September 30, when the gemini-omni-flash-preview endpoint is retired. The migration target is gemini-omni-1.1-flash, GA since August 27●APIKEY — The Gemini API begins rejecting requests from standard API keys during September. If you have not moved to auth keys yet, this month is the real deadline●SHEETS — Sheets canvas, a Gemini-powered feature, began a gradual rollout to Scheduled Release domains on August 31, with up to 15 days before it becomes visible●MEET — Starting August 31, Google Meet hardware touch controllers can start, stop, and manage the Take notes for me feature directly from the in-room screen●PARAMS — temperature, top_p, and top_k remain silently deprecated: requests are accepted and the values ignored. Code that assumes determinism needs to be verified by measurement
Your New Gemini API Key Never Took Effect, and Load Order Wasn't the Reason
When several sources supply the same environment variable, swapping your key can leave the old value in place. Here is what four load orders actually produced, and a ledger that records which source won.
September arrived, and with it the one item in my Gemini code that has to be finished this month: moving from standard API keys to auth keys. The clearest place to start was the wallpaper category classification batch I run as an indie developer, so I started there.
I rewrote the value in .env to the new key, ran the job locally once, confirmed a response came back, and put it back on its schedule. The next morning's log showed requests still going out with the old key.
The .env file had definitely changed. The process was definitely reading that file. The old value was being used anyway.
Three Sources Were Fighting Over One Name
Before hunting for a cause, I listed every place that hands GEMINI_API_KEY to that process. There were three.
Supply path
Where it lives
Findable by code search?
Shell environment
An export in the scheduler's wrapper script
Yes, but in a different repo
dotenv file
.env at the project root
Filename yes, value no
Secret file
JSON placed at deploy time
No
This is where the first mistake happens. Running grep -rn "GEMINI_API_KEY" surfaces the places that read the value: the os.environ.get(...) and process.env.... lines. The places that write it live in CI settings screens, files expanded at deploy time, and wrapper scripts, which sit either outside the repository or in a corner of it.
Count only the readers and you conclude "three reads, three fixes." The entire supply-side inventory falls out of scope. I got that order wrong once.
Four Load Orders, Same Winner
My first suspicion was load order. If the last read wins, I reasoned, I just need the new key read last. It is a reasonable guess.
So I wrote the smallest possible loader with three supply paths, and ran it with the order shuffled.
# probe.py — a config loader with three supply paths; shuffle the order, watch the winnerimport os, sys, jsonfrom dotenv import load_dotenvdef from_shell(): # only reads what is already in the process; writes nothing return os.environ.get("GEMINI_API_KEY")def from_dotenv(): # python-dotenv defaults to override=False: it will not write over an existing value load_dotenv("/tmp/keyaudit/.env") return os.environ.get("GEMINI_API_KEY")def from_secretfile(): # a plain assignment; overwrites unconditionally d = json.load(open("/tmp/keyaudit/secrets.json")) os.environ["GEMINI_API_KEY"] = d["GEMINI_API_KEY"] return os.environ["GEMINI_API_KEY"]fns = {"shell": from_shell, "dotenv": from_dotenv, "secretfile": from_secretfile}for name in sys.argv[1].split(","): fns[name]()print(f"order={sys.argv[1]:32s} winner={os.environ.get('GEMINI_API_KEY')}")
With the new key in .env, the old key in the secret file, and the old key in the shell, I tried four orders:
All four produced the same winner: the secret file holding the old key. Rearranging the sequence moved nothing.
The reason is that each path has its own policy toward an existing value. load_dotenv() respects what is already there and declines to write. A plain os.environ[...] = value overwrites without looking. So the outcome is not decided by who reads last. It is decided by this: if any path grants itself permission to overwrite, that path becomes the final winner.
Until those four lines printed, I had been shuffling import statements. While you believe the cause is ordering, no amount of reordering changes the symptom, so you gather no new information.
✦
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 will be able to tell, before you start a key migration, which of your supply paths are allowed to overwrite and which ones fail silently
✦You will be able to drop a ledger into your own runtime that records which key actually reached the request, without ever writing the key value to a log
✦You will be able to explain why shuffling four different load orders changed nothing, and skip the detour of rearranging import statements to fix it
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.
Look at parsed_seen. The parsed property on the return value holds the new key. The file was read, and the value was parsed correctly. process.env is still the old one.
Mid-debug, when you want to check whether .env is really being read, printing result.parsed shows you the new value. Read that as "it works" and the actual problem, that nothing was injected, drops out of view. That cost me the better part of an hour.
The single honest signal is the injected env (0) line, which is easy to lose among other output. Python behaves the same way: the value did not change across load_dotenv().
Situation
python-dotenv 1.2.2
dotenv (Node) 17.4.2
Variable unset
File value is applied
File value is applied
Variable already set, default options
No change
No change (injected env (0))
Variable already set, override enabled
Overwritten by file value
Overwritten by file value
Return value
Load success only
parsed exposes the new value
Nothing raises. Status codes stay at 200. This shape, where an assumption quietly stops holding, mirrors the way the deprecated temperature and top_p parameters are accepted and then ignored. A change that stops your job is easier to live with than one that does not.
Put the Overwrite Policy in One Place
With the cause identified, the fix was to make "which path may overwrite" an explicit, single decision. Leaving implicit overrides scattered across the codebase means the next migration stalls in the same spot.
The approach:
Route every supply through one function instead of a direct os.environ[...] = ...
Keep the set of paths allowed to overwrite inside that function
Record, at that moment, whether the supply was actually applied
The third point carries the weight. "I handed over the new key" and "the new key was adopted" are different facts. This whole failure began with me treating them as one.
Record Both Supply and Read in a Ledger
Here is the audit shim I ended up with. It does not replace os.environ itself; replacing it drags in library behavior I do not fully control. It only pulls supply and read through my own functions.
"""Record where a key came from, at the moment it is read."""import os, jsonclass KeyLedger: def __init__(self): self.writes = [] self.reads = [] def supply(self, name, value, source): prev = os.environ.get(name) # apply when nothing is there; otherwise ask whether this source may overwrite applied = value is not None and (prev is None or self.policy(source)) self.writes.append({ "name": name, "source": source, "had_previous": prev is not None, "applied": bool(applied), "fingerprint": self.fp(value), }) if applied: os.environ[name] = value return applied def policy(self, source): # the single place where overwrite permission is granted return source in {"secret-manager", "explicit-cli-flag"} def read(self, name): v = os.environ.get(name) owner = next((w["source"] for w in reversed(self.writes) if w["name"] == name and w["applied"]), "pre-existing-process-env") self.reads.append({"name": name, "resolved_from": owner, "fingerprint": self.fp(v)}) return v @staticmethod def fp(v): # never keep the value; first four chars, last four chars, and length if v is None: return None return f"{v[:4]}…{v[-4:]}(len={len(v)})" def report(self): return json.dumps({"writes": self.writes, "reads": self.reads}, ensure_ascii=False, indent=2)
Run under the assumption that CI already exported the old key:
That applied: false line is the entire piece of information I was missing. "A path supplied the new key and was not adopted" now fits on one row.
The fingerprint keeps only a fingerprint by design. Writing key values into this ledger would turn the ledger itself into a new leak path. First four characters, last four, and length were enough to tell old from new. The length difference of 24 versus 32 also makes visual triage quick.
Keeping resolved_from means that during an incident, the supply source you need to fix reads straight off the record. My sense is that this single line is the difference between a short investigation and a long one.
Order the Migration by Supply Source, Not by Code
After the ledger was in, I rebuilt the migration order. It used to be "fix the code, run it, ship if it passes," which pushes the supply-side inventory to the very end.
Now it goes:
List every place that suppliesGEMINI_API_KEY, working from runtime configuration rather than code search
Decide on exactly one path that is allowed to overwrite
Put the new key into that one path
Confirm the ledger reports applied: true
Only then remove the old key from the remaining paths
Step 5 belongs last. Delete the old key first and you lose your only way to learn which path was actually in effect. Since adopting the rule of seeing applied: true with my own eyes before deleting anything, half-migrated cutovers have stopped happening.
This month holds two deadlines with genuinely different characters.
Deadline
Target
How you find it
September 30
Removal of the gemini-omni-flash-preview endpoint
The string is in your code. Search finds it
During September
Standard API keys start being rejected
The value is spread across configuration. Search does not find it
The first is a model ID replacement, so a string search across the repository catches every instance. The work is easy to plan and easy to declare finished. I covered the deadline itself in taking inventory of what depends on the September 30 removal.
The second is the subject of this article. Because the values live outside the code, no search will tell you that you are done. The only way to judge is to look at what actually reached the running process. Running several scheduled jobs on my own, I now start with whichever deadline offers the fewest ways to verify completion, rather than whichever has the fewest days left.
Write down, today, every place in your project that suppliesGEMINI_API_KEY. Not the places that read it, the places that put a value there. If you find three or more, check how many of them are allowed to overwrite. If the answer is two or more, a key swap that silently does nothing can happen to you at any time.
I spent real time on the wrong hypothesis about load order before getting here. If this spares even one person the same detour, the writing was worth 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.