Same prompt. Same code. Same SDK version. The only difference was the region — and yet asia-northeast1 and us-central1 were returning noticeably different response lengths.
This was right after July 15, when gemini-flash-latest began resolving to Gemini 3.5 Flash. My setup was the kind of thing an indie developer ends up with: Tokyo for users in Japan, us-central1 for everyone else, the same alias string written in both paths because writing it twice felt like duplication. For a few days, those two strings meant two different models.
The awkward part was that nothing broke. Both responses were valid. The JSON parsed. All that happened is that a week of notes — "Tokyo feels faster," "the English output runs shorter" — turned out to measure something other than what I thought. I had been measuring model differences and calling them regional differences.
An alias is not a global name
I had quietly assumed gemini-flash-latest was a global shortcut: one name, one underlying model, no matter where you call it from. That is not what it is. An alias is a local name for whatever the current recommended version happens to be in that region.
When a new version reaches GA, the rollout advances region by region. Not everywhere at once. So an alias does not give you these three things simultaneously:
| What I assumed | What is actually true |
|---|---|
| Same model whenever I call | Swaps without notice during rollout |
| Same model wherever I call from | Resolution skews per region for a window |
| I get told when it changes | Invisible unless you read the response |
The first row is fairly well known. The second is the one I had never accounted for — and pinning, the usual advice, only addresses the first. Anywhere I had left an alias in place across multiple regions, pinning was doing nothing for me.
The good news: the response tells you which model actually answered. That field is model_version.
# What this solves: find out which concrete version answers an alias in each
# region. While these disagree, any cross-region number you collect is
# contaminated by a model difference rather than a regional one.
import os
from google import genai
def resolve(location: str, alias: str = "gemini-flash-latest") -> str:
client = genai.Client(
vertexai=True,
project=os.environ["GOOGLE_CLOUD_PROJECT"],
location=location,
)
res = client.models.generate_content(
model=alias,
contents="ok", # we only want the resolution, so keep input minimal
)
return res.model_version
for loc in ("asia-northeast1", "us-central1"):
print(f"{loc:16s} -> {resolve(loc)}")Sample output:
asia-northeast1 -> gemini-3.5-flash
us-central1 -> gemini-3.1-flash
Those two lines were enough to make me close that week's measurement notes. The "ok" payload is deliberate — the resolution is all I need, so there is no reason to spend input tokens. At a few tokens per call and $1.50 per million input tokens for gemini-3.5-flash, running this across two regions daily costs effectively nothing.
Skew corrupts more than your comparisons
Working through what actually breaks during the skew window, the damage fell into three directions.
Comparisons and canaries. Any design that diffs region A against region B silently assumes both are the same model. Once that assumption fails, you cannot attribute the difference to anything.
Latency numbers. "Tokyo's p95 is 200ms faster" reads like a network-distance story. If two model generations are mixed in, it might be inference time instead.
Caching. This one hurt most quietly. My response cache keyed on alias + hash(prompt). So output generated by gemini-3.5-flash in Tokyo got stored under the name gemini-flash-latest, and a us-central1 request read it back. If you share a cache across regions, you end up serving one model's output as another model's output.
The fix was a single line: key on what answered, not on what you asked for.
# What this solves: put the concrete version that answered into the cache key
# instead of the alias name, so two different models can never share a key
# during a rollout.
import hashlib
def cache_key(model_version: str, prompt: str) -> str:
# model_version comes from res.model_version (e.g. "gemini-3.5-flash").
# Keying on the alias ("gemini-flash-latest") means that the moment
# resolution moves, stale output is served under the new model's name.
digest = hashlib.sha256(prompt.encode("utf-8")).hexdigest()[:32]
return f"{model_version}:{digest}"On write you obviously have model_version in hand; on read you have not called yet, so you do not. I resolved that by having the read path reuse the region's resolution once per day. Less precise than I would like, but keeping two models out of one key mattered more than elegance.