◉GEMINI LABJP
●ARGON — gemini-4-argon is generally available as of 10/13, aimed at complex coding, agents, math and science●DEEP RES — deep-research-pro-preview-12-2025 shuts down 10/23, 6 days left. Move to deep-research-preview-04-2026●NANO — gemini-nano-banana-2.1 is GA (10/06). gemini-3.1-flash-image is deprecated with no shutdown date yet●MCP — One write-up cut an 85,000-token tool result to 310 tokens by wrapping it in an ADK FunctionTool●FLASH — gemini-3.7-flash and 3.5-flash now route automatically to newer Flash models (10/08)●NEW — Stop Re-Reading Every Crash Report: Gre…●ARGON — gemini-4-argon is generally available as of 10/13, aimed at complex coding, agents, math and science●DEEP RES — deep-research-pro-preview-12-2025 shuts down 10/23, 6 days left. Move to deep-research-preview-04-2026●NANO — gemini-nano-banana-2.1 is GA (10/06). gemini-3.1-flash-image is deprecated with no shutdown date yet●MCP — One write-up cut an 85,000-token tool result to 310 tokens by wrapping it in an ADK FunctionTool●FLASH — gemini-3.7-flash and 3.5-flash now route automatically to newer Flash models (10/08)●NEW — Stop Re-Reading Every Crash Report: Gre…
Articles/API / SDK
◈ API / SDK/2026-04-19Advanced

Gemini API Caching in Production — Operational Notes from an Indie Mobile Developer

Field notes on running Gemini API's Context Caching and Implicit Caching together inside indie mobile apps. Includes working Python code, six months of measured costs from AdMob-funded apps, and seven non-obvious operational pitfalls.

gemini-api286cachingcost-optimization30python108production140

✦ Premium Article

When you run a family of indie iPhone and Android apps — wallpapers, calming visuals, manifestation tools — monetised mostly through AdMob, the monthly Gemini API bill comes straight out of ad revenue. That framing matters for this article: caching isn't an abstract optimisation problem for me. The API bill is the same kind of line item as my electricity bill.

The trigger for taking caching seriously was a small one. I added a feature to one of the wallpaper apps that generates a short poem matching the mood of each image. The first month's Cloud Billing email said $412 — about three times what I'd expected — and the cause was almost entirely the system prompt and brand guidelines being shipped on every request. AdMob revenue was still in the black, but I sat with that bill for a while before opening the docs on Context Caching and Implicit Caching.

What follows is what six months of operating these two caching mechanisms in production taught me. Every code sample has been verified in a working system. Numbers come from real Cloud Billing and AdMob reports.

Why Caching Matters: The Gemini API Cost Structure

Before diving into implementation, it helps to understand exactly where the money goes.

Gemini API billing splits into three buckets: input tokens, output tokens, and cache storage. The first one is the killer: input tokens are billed on every single request.

Consider a customer support bot with a 10,000-token system prompt that handles 1,000 requests per day. That's 10 million tokens per day, 300 million per month. At gemini-2.5-pro pricing (~$1.25 per million input tokens up to 200K), the system prompt alone costs about $375 a month — and the user hasn't received a single useful answer yet.

Caching changes the equation:

  • Context Caching: Explicitly cache a chunk of content and reference it by ID in subsequent requests. Cached tokens are billed at approximately a 75% discount versus full price.
  • Implicit Caching: When the same prompt prefix repeats, discounted pricing kicks in automatically (enabled by default on Gemini 2.5 and newer models).

Picking the right one for each call site — and combining them deliberately — is what actually drives bills down.

What Caches Well, and What Doesn't

Not every payload benefits. The big wins come from "fixed context reused across requests":

  • Long system prompts (5,000+ tokens of spec, instructions, brand guidelines)
  • Reference documents (product manuals, API specs, legal text)
  • Image and video files used as multimodal context
  • Whole codebases used for code review or Q&A

User messages that change every call are not cacheable — and trying to design as if they were just wastes effort. Separating "the part that never changes" from "the part that does" is the first real step toward caching efficiency.

In my wallpaper app, the fixed context — world-building, tone rules, banned-word list, output format spec — adds up to about 12,400 tokens. The user message is just an image embedding and a couple of lines of instruction. That split alone took the monthly bill from $281 to $98 over six months, with $96 as the lowest month.

For the other levers beyond caching, see How I Cut My Gemini API Bill from ¥52,000 to ¥8,400 a Month.

How Context Caching Works (and How to Implement It)

Context Caching is an explicit API: create the cache, get back an ID, then reference that ID from later requests to get the discount.

Prerequisites — Check These First

There are a few conditions that aren't obvious until you've tripped on them:

  • Minimum token count: The cached content has to clear a per-model floor — and that floor moves between generations. Table in the next section.
  • API surface: Explicit Context Caching lives on the generateContent family of APIs. The Interactions API supports implicit caching only — more on that below.
  • Cache TTL: One hour by default; extend it with ttl or expire_time.

The minimum-token requirement is easy to miss; cache something shorter and you'll get a BadRequestError.

The Morning the Discount Disappeared

Some time after I first wrote this, I moved the wallpaper tagging batch onto a newer Flash generation. The next morning's report had runs where cached_content_token_count came back as zero. Not one line of my code had changed. I suspected TTL, then display_name, and eventually found it: the minimum token count that lets caching kick in is different per model generation.

Here is what the official Context caching page lists as of September 2026:

ModelMin token limit
Gemini 3.8 Flash4,096
Gemini 3.7 Flash4,096
Gemini 3.6 Flash4,096
Gemini 3.5 Flash4,096
Gemini 3.1 Pro Preview4,096
Gemini 2.5 Flash2,048
Gemini 2.5 Pro2,048

The "2,048 tokens is enough" assumption that held on the 2.5 generation does not hold on the 3.x line. What makes this expensive is the failure mode: a prompt that falls under the floor throws no error and logs no warning — it is simply billed without the discount. My system prompt was 12,400 tokens, so explicit Context Caching came through untouched. The 3,100-token prompt in the tagging batch did not, and I only found out from the end-of-month invoice.

I keep the floor in a table keyed by model name rather than as a constant in the code. A hardcoded 2,048 turns into an invisible price increase the day you move generations.

# Per-model minimum token counts (September 2026, per the official docs)
IMPLICIT_CACHE_MIN_TOKENS = {
    "gemini-3.8-flash": 4096,
    "gemini-3.7-flash": 4096,
    "gemini-3.6-flash": 4096,
    "gemini-3.5-flash": 4096,
    "gemini-3.1-pro-preview": 4096,
    "gemini-2.5-flash": 2048,
    "gemini-2.5-pro": 2048,
}
 
# Default unknown models to the HIGHER floor. Defaulting low means
# undiscounted requests start slipping into the bill the day a new
# generation ships, with nothing in the logs to tell you.
UNKNOWN_MODEL_FLOOR = 4096
 
 
def cache_floor(model_name: str) -> int:
    key = model_name.replace("models/", "")
    return IMPLICIT_CACHE_MIN_TOKENS.get(key, UNKNOWN_MODEL_FLOOR)
 
 
def prefix_reaches_floor(model_name: str, prefix_tokens: int) -> bool:
    """Does the fixed part of the prompt clear this model's floor?"""
    return prefix_tokens >= cache_floor(model_name)

Run this once before a migration and you catch "we upgraded but the bill didn't move" on your own machine instead of on an invoice. I keep the table next to the config file that holds the model IDs, and a pre-deploy check fails the build when prefix_reaches_floor() returns False. For prompts that come up short, I deliberately pad the fixed section with output-format detail until it clears 4,096 — being concise matters less than being discounted, at least at the end of the month.

The Interactions API Takes Explicit Caching Off the Table

There is a second fork worth knowing about before you commit to a design. The Interactions API supports implicit caching only; the explicit Context Caching this article is built around — creating cachedContents yourself and referencing the ID — isn't available there. If you want guaranteed savings from an explicit cache, that path has to stay on the generateContent family.

In my own app I moved only the features that benefit from server-side conversation state onto the Interactions API, and left the prompt-heavy tagging and poem generation where they were. You don't have to pick one home for everything. State-heavy work and discount-critical work can live on different APIs.

One note on the samples below: they're written against google-generativeai (genai.GenerativeModel / caching.CachedContent). On the current google-genai SDK, caching.CachedContent.create() maps to client.caches.create(), .list() to client.caches.list(), and .delete() to client.caches.delete(name). Either way, the only fields an update can change are ttl and expire_time — everything else means building the cache again.

Creating a Cache (Basic Implementation)

Here's a minimal pattern for caching a long system prompt.

import google.generativeai as genai
from google.generativeai import caching
import datetime
import time
 
# In real code, pull the key from an environment variable
genai.configure(api_key="YOUR_GEMINI_API_KEY")
 
SYSTEM_DOCUMENT = """
You are an advanced customer support AI. Follow the rules and knowledge base below.
 
[Product documentation: 10,000+ characters of detailed spec]
--- Product Spec ---
Product Name: ExampleApp Pro
Version: 3.2.1
... (long content continues) ...
"""  # Use real content that adds up to 2,048+ tokens
 
def create_cache() -> caching.CachedContent:
    """Create a Context Cache and return the cached content object."""
    try:
        cached_content = caching.CachedContent.create(
            model="models/gemini-2.5-pro",
            display_name="customer-support-system-prompt",  # Choose a searchable name
            system_instruction=SYSTEM_DOCUMENT,
            ttl=datetime.timedelta(hours=12),  # Retain cache for 12 hours
        )
        print(f"OK Cache created: {cached_content.name}")
        print(f"   Expires: {cached_content.expire_time}")
        print(f"   Tokens: {cached_content.usage_metadata.total_token_count}")
        return cached_content
    except Exception as e:
        print(f"FAIL Cache creation failed: {e}")
        raise
 
def chat_with_cache(cached_content: caching.CachedContent, user_message: str) -> str:
    """Send a chat request that references the cache."""
    try:
        model = genai.GenerativeModel.from_cached_content(cached_content)
        response = model.generate_content(user_message)
 
        # Verify the cache is actually being used
        usage = response.usage_metadata
        print(f"   Cached tokens: {usage.cached_content_token_count}")
        print(f"   Prompt tokens (total): {usage.prompt_token_count}")
        print(f"   Output tokens: {usage.candidates_token_count}")
 
        return response.text
    except Exception as e:
        print(f"FAIL Request failed: {e}")
        raise
 
if __name__ == "__main__":
    cache = create_cache()
 
    questions = [
        "What is your return policy?",
        "How long is the product warranty?",
        "What are your support hours?",
    ]
 
    for question in questions:
        print(f"\nQ: {question}")
        answer = chat_with_cache(cache, question)
        print(f"A: {answer[:100]}...")
        time.sleep(1)

The line worth memorising is response.usage_metadata.cached_content_token_count. If this is zero, the cache wasn't actually used and something's wrong — monitor this value in production.

Cache Lifecycle Management

In production you need to handle list/update/delete cleanly. This manager class is what I run in my apps.

import google.generativeai as genai
from google.generativeai import caching
import datetime
from typing import Optional
 
genai.configure(api_key="YOUR_GEMINI_API_KEY")
 
class CacheManager:
    """CRUD operations for Context Cache."""
 
    @staticmethod
    def list_caches() -> list:
        """List currently active caches."""
        caches = list(caching.CachedContent.list())
        print(f"Active caches: {len(caches)}")
        for c in caches:
            now = datetime.datetime.now(datetime.timezone.utc)
            remaining = (c.expire_time - now).total_seconds()
            print(f"  - {c.display_name}: {remaining/3600:.1f}h remaining")
        return caches
 
    @staticmethod
    def get_cache_by_name(display_name: str) -> Optional[caching.CachedContent]:
        """Look up a cache by name (important for reuse across process restarts)."""
        for c in caching.CachedContent.list():
            if c.display_name == display_name:
                now = datetime.datetime.now(datetime.timezone.utc)
                remaining_hours = (c.expire_time - now).total_seconds() / 3600
                if remaining_hours > 0.5:  # Only use if 30+ minutes left
                    return c
        return None
 
    @staticmethod
    def extend_cache_ttl(cache: caching.CachedContent, hours: int = 24):
        """Extend a cache's TTL."""
        try:
            cache.update(ttl=datetime.timedelta(hours=hours))
            print(f"OK TTL extended: {cache.display_name} -> +{hours}h")
        except Exception as e:
            print(f"FAIL TTL update failed: {e}")
 
    @staticmethod
    def delete_cache(cache: caching.CachedContent):
        """Delete a cache to stop accruing storage costs."""
        try:
            display_name = cache.display_name
            cache.delete()
            print(f"DELETED Cache: {display_name}")
        except Exception as e:
            print(f"FAIL Delete failed: {e}")

get_cache_by_name matters more than it looks. If a server restart creates a fresh cache every time, you pile up storage costs for orphaned caches. Look up the existing one by name and reuse it.

Worked Example: Context Caching Cost

Concrete numbers help.

Setup: gemini-2.5-pro, 10,000-token system prompt, 1,000 requests/day

Without caching (monthly):

  • Input tokens: 10,000 × 1,000 × 30 = 300M tokens
  • Estimated cost: $375 / month

With Context Caching (monthly):

  • Cache creation + storage (12h TTL, daily refresh): ~$24 / month
  • Cache reference (75% discount on cached portion): ~$95 / month
  • Total: ~$119 / month (about 68% savings)

These numbers assume the system prompt was previously sent on every request. Real prices vary — check the Google AI pricing page for the latest.

✦

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
✦Six months of measured monthly cost on an indie wallpaper app: $281 → $98 with Context Caching, plus how the hit rate moved week by week
✦Seven implementation insights that aren't in the official docs (min-token requirements, hidden storage costs, display_name collisions, expiry races, count_tokens being billable)
✦Decision rules for combining Flash + Pro with caching in AdMob-funded apps, a TTL cheat sheet by task type, and a 'monthly budget ÷ 30 × 1.3' billing alert formula
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.

or
Unlock all articles with Membership →
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.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

Related Articles

◈ API / SDK2026-04-07
Gemini API Semantic Router: Implementation Notes for Splitting Flash and Pro Smartly
Implementation notes for building a production-grade semantic router that automatically dispatches Gemini queries between Flash and Pro. Includes Python and TypeScript working code, a two-stage design pattern, and seven implementation insights from running it inside an indie wallpaper app.
◈ API / SDK2026-07-15
Sample what you already accepted — an audit budget that catches silent quality drift
A confidence gate only ever looks at output the model hesitated on. Silent drift sinks into the batch that sailed through. Working from a fixed 30-minutes-a-day review budget, this walks through deriving detection time from the binomial, reallocating the same budget across risk strata, and catching slow decay with a cumulative monitor.
◈ API / SDK2026-07-04
When Gemini API Leaks Japanese Into Your English Output Once in a While — Field Notes on Measuring the Contamination Rate and Tightening It in Stages
You told Gemini to answer in English, and 3 out of 100 runs slip a Japanese sentence into the tail. Here is why you cannot stop that 'once in a while', and a production pattern that measures the contamination rate as an SLO and tightens it with graded recovery, with working code.