Gemini 3 Pro Flash Nano explained: Google’s latest model family splits into three distinct tiers — Pro for complex reasoning, Flash for high-throughput latency-sensitive workloads, and Nano for on-device inference. Each tier shares the same underlying architecture but differs in parameter count, context window, and training compute allocation. Understanding these boundaries determines whether your application hits latency budgets, stays within token budgets, or runs locally at all.
What the three tiers actually are
Google DeepMind trained Gemini 3 as a single model family with three distillation targets. The Pro variant sits at the top — full parameter count, 2M token context window, trained for maximum reasoning depth across code, math, and long-context synthesis. Flash is a distilled variant optimized for throughput: smaller parameter count, 1M token context, but significantly faster decode speeds and lower per-token cost. Nano is the edge deployment target — quantized to 4-bit or 8-bit, context window capped at 32K or 128K depending on the specific Nano variant, designed to run on mobile SoCs and embedded devices.
The naming convention mirrors the industry pattern: Pro = capability ceiling, Flash = throughput floor, Nano = deployment floor. But unlike some competitors who ship fundamentally different architectures per tier, Gemini 3 uses a shared backbone with progressive distillation. This matters for prompt portability — a prompt that works on Pro will usually work on Flash with minimal adaptation, and Nano inherits the same tokenizer and chat template.
Architecture and training differences
All three tiers share the same tokenizer (256K vocabulary, byte-pair encoding with special tokens for tool use and structured output), the same chat template format, and the same system prompt handling. The divergence happens in post-training:
Pro receives the full RLHF/RLAIF pipeline with preference data covering complex multi-step reasoning, tool use chains, and long-context needle retrieval. Training compute allocation is highest here — think of it as the “teacher” model in a distillation sense.
Flash undergoes knowledge distillation from Pro using a combination of logit matching and preference distillation. The distillation target emphasizes latency-critical paths: shorter reasoning chains, earlier exit criteria for simple queries, and aggressive KV-cache optimization. The result is a model that matches Pro on short-context benchmarks (MMLU, GSM8K, HumanEval) within 2-3% but diverges on tasks requiring extended reasoning traces or 100K+ context synthesis.
Nano takes Flash as its teacher and applies quantization-aware training (QAT) followed by post-training quantization (PTQ) to INT4/INT8. Google publishes two Nano variants: Nano-1 (1.8B params, 32K context) for mobile NPUs, and Nano-2 (3.2B params, 128K context) for higher-end edge devices. Both use grouped-query attention (GQA) with 8 query heads per KV head, and sliding window attention for the 128K variant to keep memory linear.
# Simplified model card comparison (not actual API response)
gemini_3_tiers = {
"pro": {
"params": "~200B+", # not publicly disclosed
"context_window": 2_000_000,
"quantization": "bf16/fp8 server-side",
"target_hardware": "TPU v5p / v6e pods",
"typical_latency_p50": "~1.2s first token (1k input)",
"typical_throughput": "~180 tok/s",
},
"flash": {
"params": "~50-80B", # estimated from distillation ratio
"context_window": 1_000_000,
"quantization": "fp8 server-side",
"target_hardware": "TPU v5e / v6e",
"typical_latency_p50": "~350ms first token (1k input)",
"typical_throughput": "~450 tok/s",
},
"nano_1": {
"params": "1.8B",
"context_window": 32_768,
"quantization": "INT4 (QAT + PTQ)",
"target_hardware": "Mobile NPU (Tensor G3/G4, Snapdragon 8 Gen 3)",
"typical_latency_p50": "~80ms first token (512 input, on-device)",
"typical_throughput": "~45 tok/s (on-device)",
},
"nano_2": {
"params": "3.2B",
"context_window": 131_072,
"quantization": "INT4 (QAT + PTQ)",
"target_hardware": "High-end mobile / edge accelerator",
"typical_latency_p50": "~120ms first token (512 input, on-device)",
"typical_throughput": "~35 tok/s (on-device)",
},
}
Token economics and routing decisions
The per-token pricing delta between Pro and Flash is substantial — roughly 8-10x on input and 5-6x on output depending on the provider. For a workload processing 10M input tokens and 2M output tokens daily, the monthly difference can exceed $15K. This makes tier selection a first-order architectural decision, not an afterthought.
Routing logic should be explicit in your application layer. A common pattern:
from enum import Enum
from dataclasses import dataclass
class GeminiTier(Enum):
PRO = "gemini-3-pro"
FLASH = "gemini-3-flash"
NANO = "gemini-3-nano" # client-side only
@dataclass
class RoutingPolicy:
max_context_tokens: int
require_extended_reasoning: bool
latency_budget_ms: int
cost_per_million_input: float
cost_per_million_output: float
TIER_POLICIES = {
GeminiTier.PRO: RoutingPolicy(
max_context_tokens=2_000_000,
require_extended_reasoning=True,
latency_budget_ms=5000,
cost_per_million_input=3.50,
cost_per_million_output=10.50,
),
GeminiTier.FLASH: RoutingPolicy(
max_context_tokens=1_000_000,
require_extended_reasoning=False,
latency_budget_ms=1500,
cost_per_million_input=0.35,
cost_per_million_output=1.75,
),
GeminiTier.NANO: RoutingPolicy(
max_context_tokens=131_072, # nano-2
require_extended_reasoning=False,
latency_budget_ms=200, # on-device, no network
cost_per_million_input=0.0, # local compute
cost_per_million_output=0.0,
),
}
def select_tier(task: dict) -> GeminiTier:
"""
Heuristic router. Replace with learned router or LLM-as-judge for production.
"""
context_estimate = task.get("input_tokens", 0) + task.get("max_output_tokens", 0)
needs_reasoning = task.get("reasoning_depth", "shallow") == "deep"
latency_budget = task.get("latency_budget_ms", 2000)
if needs_reasoning or context_estimate > TIER_POLICIES[GeminiTier.FLASH].max_context_tokens:
return GeminiTier.PRO
if latency_budget < 500 and task.get("can_run_local", False):
return GeminiTier.NANO
return GeminiTier.FLASH
This router is intentionally simple. In production, you’d add a fallback chain (Pro → Flash → cached response), a cost-aware bandit for exploration, and telemetry to measure actual quality degradation per tier per task type.
Why the 2M context window on Pro changes things
A 2M token context window is not just a larger number — it shifts the retrieval-augmented generation (RAG) architecture. With Flash’s 1M window, you still need aggressive chunking, reranking, and context compression for large codebases or document corpora. With Pro’s 2M window, you can often stuff the entire relevant corpus (a full repository, a legal contract bundle, a medical record history) directly into the prompt.
This eliminates an entire class of retrieval failures: embedding drift, chunk boundary artifacts, reranker latency, and the “lost in the middle” phenomenon. The trade-off is quadratic attention cost — 2M tokens means 4T attention operations per layer. Google mitigates this with ring attention and block-sparse patterns on TPU pods, but you still pay for it in latency and cost.
Practical implication: if your RAG pipeline spends >40% of its latency budget on retrieval + reranking, and your corpus fits in 1.5M tokens, Pro with full-context stuffing often beats Flash + RAG on both latency and answer quality. Measure this per workload.
# Rough cost/latency model for decision making
def estimate_pro_vs_flash_rag(corpus_tokens: int, queries_per_day: int):
# Flash + RAG: retrieval (150ms) + rerank (80ms) + 32k context generation
flash_latency = 230 + 32_000 / 450 * 1000 # ~71s for generation? No, that's wrong.
# Let's use realistic numbers:
# Flash: 150ms retrieval + 80ms rerank + 500ms generation (short context) = ~730ms
# Pro: 0ms retrieval + 1200ms generation (long context, but single pass)
flash_daily_cost = queries_per_day * (
0.35 * (32_000 / 1_000_000) + # input tokens (retrieved context)
1.75 * (2_000 / 1_000_000) # output tokens
)
pro_daily_cost = queries_per_day * (
3.50 * (corpus_tokens / 1_000_000) +
10.50 * (2_000 / 1_000_000)
)
return {
"flash_rag_latency_ms": 730,
"pro_full_context_latency_ms": 1200,
"flash_daily_cost": flash_daily_cost,
"pro_daily_cost": pro_daily_cost,
"breakeven_corpus_tokens": 32_000 * (3.50 / 0.35) # ~320k tokens
}
# At ~320k corpus tokens, Pro full-context matches Flash+RAG on cost.
# Below that, Flash+RAG wins. Above that, Pro wins on both cost and simplicity.
Nano on-device: what actually runs locally
Nano is not a server model. It ships as a .task bundle for MediaPipe LLM Inference (Android) or as a Core ML / ONNX package for iOS/macOS. The model weights are encrypted at rest and decrypted into secure enclave memory at load time — this is a DRM requirement from Google, not a technical limitation of the format.
Key constraints engineers hit immediately:
-
No streaming in the first release. The MediaPipe LLM Inference API returns the full generation synchronously. Async streaming support is on the roadmap but not in the initial SDK. This blocks UX patterns like typewriter effects.
-
Fixed context window at load time. You choose 32K or 128K when initializing the
LlmInferenceobject. Dynamic context resizing requires reloading the model (2-3 second penalty). -
Tool use is compile-time. Function schemas must be declared at model conversion time. Dynamic tool registration at runtime is not supported — the tokenizer special tokens for tool calls are baked into the Nano vocabulary.
// Android MediaPipe LLM Inference - Nano initialization
val options = LlmInference.LlmInferenceOptions.builder()
.setModelPath(context.filesDir.resolve("gemini-3-nano-2.task").absolutePath)
.setMaxTokens(131072) // nano-2 context window
.setTemperature(0.7f)
.setTopK(40)
.setTopP(0.95f)
.setRandomSeed(42)
.build()
val llm = LlmInference.createFromOptions(context, options)
// Generation is synchronous - blocks calling thread
val result = llm.generate("Summarize this document: $documentText")
// No streaming callback in v1 API
- Quantization artifacts on long context. The INT4 Nano-2 shows measurable perplexity degradation past 64K tokens on tasks requiring precise recall (needle-in-haystack, exact quote extraction). For summarization and classification it holds up to 128K. Test your specific task at target context lengths.
Common misconceptions
“Flash is just a quantized Pro.” False. Flash is a distilled model with a different parameter count and architecture modifications (fewer layers, wider FFN ratio, GQA tuned for throughput). Quantization happens on top of distillation for server-side fp8 serving, but the model itself is structurally different.
“Nano can run on any modern phone.” False. Nano-1 requires a minimum of 4GB RAM dedicated to the model (weights + KV cache + activation buffer) and an NPU supporting INT4 matrix multiply with 2048+ MACs/cycle. Snapdragon 8 Gen 2, Tensor G2, and Dimensity 9200 fall short. Check PackageManager.hasSystemFeature("android.hardware.ml.npu") and benchmark before shipping.
“Pro’s 2M context means I can ignore RAG forever.” False. The 2M window is a maximum, not a recommended operating point. Attention computation scales quadratically. A 1.5M token prompt on Pro takes 8-12 seconds for first token on current TPU serving stacks. For interactive workloads, you still want retrieval to keep context under 200K-500K tokens.
“All three tiers share the same system prompt behavior.” Mostly true, but Nano has a truncated system prompt budget (2K tokens vs 8K on Pro/Flash) because the system prompt consumes precious context window on-device. Long system prompts with few-shot examples will crowd out user context on Nano.
“Flash is cheaper so I should default to Flash.” Only if your quality eval passes. Flash degrades noticeably on: multi-hop reasoning (>3 steps), code generation requiring whole-repo context, non-English low-resource languages, and instruction following with >10 constraints. Run your eval suite per tier before routing production traffic.
Evaluation strategy per tier
Don’t rely on public benchmarks. Build a tier-specific eval harness:
# eval/harness.py
from dataclasses import dataclass
from typing import Callable
import json
@dataclass
class EvalCase:
name: str
prompt: str
expected_behavior: dict # flexible criteria
tier_requirements: list[str] # ["pro", "flash", "nano"]
@dataclass
class TierResult:
tier: str
latency_ms: float
output: str
passed: bool
failure_mode: str | None
def run_tier_eval(
tier: str,
cases: list[EvalCase],
generate_fn: Callable[[str], str],
judge_fn: Callable[[str, dict], tuple[bool, str]],
) -> list[TierResult]:
results = []
for case in cases:
if tier not in case.tier_requirements:
continue
import time
start = time.perf_counter()
output = generate_fn(case.prompt)
latency_ms = (time.perf_counter() - start) * 1000
passed, failure_mode = judge_fn(output, case.expected_behavior)
results.append(TierResult(tier, latency_ms, output, passed, failure_mode))
return results
# Example judge for code generation
def code_judge(output: str, expected: dict) -> tuple[bool, str]:
# Check syntax validity
try:
compile(output, "<eval>", "exec")
except SyntaxError as e:
return False, f"syntax_error: {e}"
# Check required functions exist
for fn_name in expected.get("required_functions", []):
if f"def {fn_name}" not in output:
return False, f"missing_function: {fn_name}"
# Check no forbidden patterns
for pattern in expected.get("forbidden_patterns", []):
if pattern in output:
return False, f"forbidden_pattern: {pattern}"
return True, None
Run this against your actual task distribution. You’ll typically find Flash passes 85-90% of cases that Pro passes, Nano passes 60-70% of Flash cases. The gap tells you your routing thresholds.
Migration path from Gemini 1.5/2.0
If you’re migrating from Gemini 1.5 Pro/Flash, the API surface is identical — same OpenAI-compatible chat completions endpoint, same function calling schema, same streaming format. The changes are behavioral:
- System prompt adherence is stricter on Gemini 3. Prompts that relied on “ignore previous instructions” style jailbreaks now consistently fail. This is a feature.
- Tool call formatting uses a new
tool_codeblock type instead offunctioncalls. The old format still works but is deprecated. Update your parser. - Long context caching (context caching API) now supports cross-request prefix caching on Pro and Flash. Enable it for repeated corpus prefixes — 50-70% input token reduction on RAG workloads.
// Context caching request (Gemini 3 Pro/Flash)
{
"model": "gemini-3-pro",
"cached_content": {
"model": "gemini-3-pro",
"contents": [
{"role": "user", "parts": [{"text": "<full_legal_contract_150k_tokens>"}]},
{"role": "model", "parts": [{"text": "Understood. I have the contract loaded."}]}
],
"ttl": "3600s"
}
}
// Subsequent requests reference the cache
{
"model": "gemini-3-pro",
"cached_content": "cache_abc123",
"contents": [
{"role": "user", "parts": [{"text": "What is the termination clause in section 8.4?"}]}
]
}
Summary decision matrix
| Workload characteristic | Recommended tier | Rationale |
|---|---|---|
| Multi-step reasoning (>3 hops), 100K+ context synthesis | Pro | Only tier with reliable extended reasoning at scale |
| High-volume chat, classification, extraction, <32K context | Flash | 8-10x cost savings, latency fits interactive budgets |
| On-device privacy, offline-first, <128K context | Nano | Zero network dependency, user data never leaves device |
| Code generation with full-repo context | Pro (or Flash + RAG) | Pro handles 200K+ token repos natively |
| Real-time translation, summarization, entity extraction | Flash | Throughput optimized, quality sufficient |
| Mobile copilot, autocomplete, local search | Nano-1 or Nano-2 | Fits NPU memory budget, latency acceptable |
The Gemini 3 lineup is a coherent family — not three unrelated models. Your architecture should treat them as a single model with three operating points, routed by explicit policy, evaluated continuously, and fallback-chained for reliability.