●NANOLITE — Nano Banana 2 Lite is here: Google's fastest and most cost-efficient Gemini Image model, made for running lightweight image generation cheaply●OMNIFLASH — Gemini Omni Flash is in public preview, a natively multimodal model that lets enterprises and developers build custom, dynamic video workflows●AGENTS — Managed Agents expand with background: true for async server-side runs and polling, remote MCP server integration, and refreshing credentials across interactions●MEMORY — The Memory Bank IngestEvents API is generally available, decoupling event ingestion from memory generation so you can stream content continuously●THROUGHPUT — Provisioned Throughput now lets you submit up to seven pending orders for the same model and region●DEPRECATE — Image generation models shut down on August 17, and the Grok 4.1 family on the Gemini Enterprise Agent Platform on August 20●NANOLITE — Nano Banana 2 Lite is here: Google's fastest and most cost-efficient Gemini Image model, made for running lightweight image generation cheaply●OMNIFLASH — Gemini Omni Flash is in public preview, a natively multimodal model that lets enterprises and developers build custom, dynamic video workflows●AGENTS — Managed Agents expand with background: true for async server-side runs and polling, remote MCP server integration, and refreshing credentials across interactions●MEMORY — The Memory Bank IngestEvents API is generally available, decoupling event ingestion from memory generation so you can stream content continuously●THROUGHPUT — Provisioned Throughput now lets you submit up to seven pending orders for the same model and region●DEPRECATE — Image generation models shut down on August 17, and the Grok 4.1 family on the Gemini Enterprise Agent Platform on August 20
Gemini API Implicit Caching Not Working — Troubleshooting Guide by Root Cause
Troubleshoot Gemini API implicit caching issues: cache not hitting, unexpectedly high costs, or low cache hit rates. Covers token thresholds, prompt structure, model version consistency, TTL expiry, and multimodal caching with code examples.
When I started using Gemini 2.5 Pro for a personal project, my first billing statement caught me off guard. The documentation promised up to 75% cost reduction through implicit caching — but I was seeing almost none of that benefit.
It turned out the cause was straightforward: I wasn't meeting several prerequisites for caching to actually work. Since this seems to trip up a lot of developers, here's a systematic breakdown of what to check and how to fix it.
What Is Implicit Caching? (Quick Recap)
In Gemini 2.5 Pro and 2.5 Flash, the API automatically caches the leading portion of a prompt when the same content is sent repeatedly, applying a discounted rate to cached input tokens. Unlike explicit caching (where you manually create a CachedContent resource), implicit caching happens automatically on the API side.
The catch is that several conditions must be met for caching to kick in. If any of them are missed, you get zero cache hits — and a larger-than-expected bill.
Issue 1: Input Token Count Below the Caching Threshold
This is the most common cause. Implicit caching in Gemini 2.5 Pro only activates when input tokens exceed a minimum threshold (approximately 1,024 tokens as of May 2026).
import google.genai as genaiclient = genai.Client()# Short prompts like this won't be cachedresponse = client.models.generate_content( model="gemini-2.5-pro", contents="Implement a Fibonacci sequence in Python")meta = response.usage_metadataprint(f"Input tokens: {meta.prompt_token_count}")print(f"Cached tokens: {meta.cached_content_token_count}")# cached_content_token_count: 0
# Long system instructions + repeated calls will trigger cachingSYSTEM_PROMPT = """You are a senior Python engineer. Follow these coding standards:- PEP 8 compliance required- Type hints on all functions- Google-style docstrings- Explicit error handling(... thousands of tokens of detailed instructions ...)"""response = client.models.generate_content( model="gemini-2.5-pro", contents=[ {"role": "user", "parts": [{"text": SYSTEM_PROMPT + "\n\nImplement a Fibonacci sequence in Python"}]} ])meta = response.usage_metadataprint(f"Cached tokens: {meta.cached_content_token_count}")# On the 2nd+ call: cached_content_token_count will be a positive value
Fix: Check usage_metadata.prompt_token_count. If your prompt is consistently short, implicit caching won't help. Consider explicit caching (CachedContent) for medium-length but repetitive fixed content.
✦
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
✦Aggregation code to continuously monitor implicit cache hit rate in production and catch regressions
✦How to find prompt prefixes that look fixed but silently break the leading match
✦Clear criteria and a cost comparison for moving from implicit to explicit caching
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.
Issue 2: Cacheable Content Is Not at the Beginning
Implicit caching works by matching a consecutive block starting from the very beginning of the prompt. Fixed content placed after variable content (like the user's question) won't be cached.
# Wrong: system instructions placed after user inputcontents_bad = [ {"role": "user", "parts": [{"text": user_question}]}, # changes every call {"role": "system", "parts": [{"text": LARGE_SYSTEM_PROMPT}]} # fixed but at the end]# Correct: fixed content first, variable content aftercontents_good = [ {"role": "system", "parts": [{"text": LARGE_SYSTEM_PROMPT}]}, # fixed, at the front {"role": "user", "parts": [{"text": user_question}]} # variable, at the back]
The same principle applies to multi-turn conversations:
def build_contents(system_prompt: str, history: list, new_message: str) -> list: """Build conversation contents with caching in mind.""" contents = [ {"role": "system", "parts": [{"text": system_prompt}]}, # fixed, always first ] for turn in history: contents.append(turn) contents.append({"role": "user", "parts": [{"text": new_message}]}) return contents
Issue 3: Model Version Inconsistency Across Calls
gemini-2.5-pro and gemini-2.5-pro-latest are treated as different endpoints with separate cache namespaces. Even if you use the exact same system instructions, switching model versions between calls means the cache starts fresh each time.
# Problem: model version varies by conditionif condition_a: model = "gemini-2.5-pro-preview-05-06"else: model = "gemini-2.5-pro" # separate cache bucket# Fix: centralize model version as a constantGEMINI_MODEL = "gemini-2.5-pro"response = client.models.generate_content( model=GEMINI_MODEL, contents=contents)
If you're using feature flags or environment variables to switch models between environments (e.g., staging vs. production), be aware that each model version maintains its own cache.
Issue 4: Not Monitoring Cache Hit Rate
Many developers wonder whether caching is working without actually checking. The usage_metadata field tells you exactly what's happening.
Log this data regularly, and you'll quickly see whether your prompt structure is working as intended.
Issue 5: Cache TTL Expiry During Batch Processing
Implicit caching entries expire after approximately one hour by default. In long-running batch jobs, the cache can silently expire mid-run, causing costs to spike unexpectedly.
def process_batch_with_cache_monitoring( items: list[str], system_prompt: str, model: str = "gemini-2.5-pro") -> list[str]: """Batch processor with cache expiry detection.""" results = [] consecutive_cache_miss = 0 for i, item in enumerate(items): contents = [ {"role": "system", "parts": [{"text": system_prompt}]}, {"role": "user", "parts": [{"text": item}]} ] response = client.models.generate_content(model=model, contents=contents) meta = response.usage_metadata if meta.cached_content_token_count == 0 and i > 0: consecutive_cache_miss += 1 if consecutive_cache_miss >= 3: print(f"⚠️ Item {i}: Cache likely expired (TTL exceeded)") consecutive_cache_miss = 0 else: consecutive_cache_miss = 0 results.append(response.text) return results
The good news: after the cache expires, the next request repopulates it automatically. Only that single request incurs the full rate before caching resumes.
Issue 6: Multimodal Content Not Caching Reliably
Implicit caching works with images and PDFs too, but the way you reference files affects cache stability.
from google.genai import types# Upload the file once via Files API to get a stable URIfile = client.files.upload(path="reference_document.pdf")# file.uri is a fixed identifier that doesn't change between callscontents = [ {"role": "system", "parts": [ types.Part.from_uri(file_uri=file.uri, mime_type="application/pdf"), {"text": "Please answer questions based on the PDF above."} ]}, {"role": "user", "parts": [{"text": user_question}]}]response = client.models.generate_content(model="gemini-2.5-pro", contents=contents)print(f"Cached tokens: {response.usage_metadata.cached_content_token_count}")
inline_data (passing raw bytes directly) also works, but requires sending identical bytes every time. Files API URIs are stable identifiers that lead to more consistent cache hits.
Monitoring Cache Hit Rate in Production
A one-off usage_metadata check is a fine first step, but in real operation the scariest problem is silent decay — caching that worked last week quietly stops working. As an indie developer reusing the same generation pipeline across several Dolice sites, I once fixed a prompt in one place and only noticed days later that a different site's hit rate had dropped because of it.
So I aggregate per-request results and report the recent hit rate — plus how much I lost versus the cached rate — broken down by model.
from collections import defaultdict, dequefrom dataclasses import dataclass, fieldNORMAL_RATE = 1.25 / 1_000_000 # Gemini 2.5 Pro standard rate (as of June 2026)CACHE_RATE = 0.31 / 1_000_000 # Cached rate@dataclassclass CacheMonitor: window: int = 200 _recent: dict = field(default_factory=lambda: defaultdict(lambda: deque(maxlen=200))) def record(self, model: str, meta) -> dict: total = meta.prompt_token_count or 0 cached = meta.cached_content_token_count or 0 ratio = cached / total if total else 0.0 self._recent[model].append(ratio) saved = cached * (NORMAL_RATE - CACHE_RATE) return {"model": model, "ratio": ratio, "saved_usd": saved} def hit_rate(self, model: str) -> float: win = self._recent[model] return sum(win) / len(win) if win else 0.0 def regressed(self, model: str, floor: float = 0.4) -> bool: # Only judge once the window is full, to avoid false alarms at startup win = self._recent[model] return len(win) >= self.window and self.hit_rate(model) < floor
On the calling side:
monitor = CacheMonitor()for item in items: response = client.models.generate_content(model=GEMINI_MODEL, contents=build_contents(item)) stat = monitor.record(GEMINI_MODEL, response.usage_metadata) if monitor.regressed(GEMINI_MODEL): # Log it and route to an alert if needed — don't swallow it print(f"⚠️ Implicit cache hit rate dropped for {GEMINI_MODEL}: {monitor.hit_rate(GEMINI_MODEL):.1%}")
Tracking the rate (not the dollar amount) makes regressions far easier to catch. When Gemini 3.5 Flash reached general availability in June 2026 and the default model shifted, hit rates quietly fell for any code that hadn't pinned its model name. Watching the rate surfaces that kind of default change as an anomaly early, before the bill does.
Eliminating Prefixes That Look Fixed but Aren't
Issue 2 covered putting fixed content first. The subtler trap is one step deeper: the "fixed" prefix you put up front is actually a little different on every call. Because implicit caching matches a contiguous run from the start of the prompt, a single variable character inside the prefix throws out everything after it.
The classic mistake I made was embedding the current time inside the system instruction.
from datetime import datetimeimport json# Before: a "fixed" prefix that changes every calldef build_system_prompt_bad(rules: dict) -> str: return ( f"Current time: {datetime.now().isoformat()}\n" # changes every call -> nothing after is cached f"Settings: {json.dumps(rules)}\n" # dict ordering drift also breaks the match "You are a senior Python engineer. Follow the conventions below." )
That prefix looks fixed but produces different bytes every time, so implicit caching almost never hits. The fix is to make the prefix deterministic.
# After: push variable parts to the back, make the fixed part deterministicdef build_system_prompt_good(rules: dict) -> str: settings = json.dumps(rules, sort_keys=True) # remove JSON ordering drift return ( "You are a senior Python engineer. Follow the conventions below.\n" f"Settings: {settings}" # sorted, so identical every time )def build_contents(system_prompt: str, user_question: str, now_iso: str) -> list: # Variable info like time goes in the user part (at the back); keep system immutable return [ {"role": "system", "parts": [{"text": system_prompt}]}, {"role": "user", "parts": [{"text": f"Reference time: {now_iso}\n{user_question}"}]}, ]
The variable elements that break prefixes tend to fall into three buckets.
Variable element
Symptom
Fix
Timestamps, random values, UUIDs
Bytes change every call; zero hits
Move variable info into the user part (at the back)
dict / set serialization order
Same content, but ordering drift breaks the match
Use json.dumps(..., sort_keys=True)
Locale-dependent number/date formatting
Prefix shifts subtly across environments
Format outside the prefix; prepend only fixed strings
When the hit rate is "non-zero but mysteriously low," dump the prefix as bytes twice and run a diff. It can differ even when it looks identical to the eye.
Deciding When to Move from Implicit to Explicit Caching
Implicit caching is convenient and config-free, but for "I need this to hit reliably" or "I reference a huge fixed corpus repeatedly," an explicit CachedContent resource is the better fit — implicit caching is best-effort and never guarantees a hit.
Here are the rules of thumb.
Situation
Better approach
Why
Short-to-medium prompts called sporadically
Implicit
No setup, no TTL management — light to operate
Tens of thousands of fixed tokens referenced frequently
Explicit
Guaranteed hits; you control TTL extension
Overnight batch reusing the same context
Explicit
Not subject to implicit TTL expiry
Conversational use where the prefix changes often
Implicit
Can't be pinned, so explicit gains little
To switch to explicit, cache the fixed corpus once and set the TTL to match your processing window.
from google.genai import types# Explicitly cache the fixed reference materialcache = client.caches.create( model="gemini-2.5-pro", config=types.CreateCachedContentConfig( system_instruction="You are an answering assistant for internal documents.", contents=[large_reference_corpus], # tens of thousands of fixed tokens ttl="3600s", # long enough to cover the whole batch ),)# Later requests just point at the cache name and hit reliablyresponse = client.models.generate_content( model="gemini-2.5-pro", contents=[user_question], config=types.GenerateContentConfig(cached_content=cache.name),)print(f"Cached tokens: {response.usage_metadata.cached_content_token_count}")
Explicit caching incurs a storage charge for holding the cache itself. The clean dividing line is: "implicit for sporadic calls; explicit when you need high frequency, large volume, and certainty." In my own setup I let conversational paths ride on implicit caching, and only push jobs like overnight bulk classification — which reference the same material hundreds of times — onto explicit caches.
Quick Diagnostic Checklist
When implicit caching isn't delivering the expected cost savings, work through this list:
Is the input token count above ~1,024? Short prompts are never cached.
Does the fixed content come first in the prompt? Content after variable input won't be cached.
Is the model name identical across all requests? Version differences fragment the cache.
Are you checking usage_metadata.cached_content_token_count? You can't improve what you don't measure.
Has the batch been running for more than an hour? TTL expiry silently resets cache.
For multimodal inputs, are you using Files API URIs? Inline byte data is less reliable for caching.
Start by logging cached_content_token_count for every request. Once you can see the numbers, the right fix usually becomes obvious.
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.