Our Gemini API bill was climbing past ¥150,000 per month. Adding one feature brought it below ¥40,000.
That feature was context caching.
It's a Gemini API capability that most developers don't know exists, yet it can eliminate 60–80% of input token costs for systems that repeatedly send the same content. I started investigating it after our RAG + agent system exceeded budget projections — every request was sending a 45,000-token system prompt, and those costs were compounding fast.
This article documents the full implementation: what context caching actually does, how the pricing math works, and the TypeScript and Python code we use in production at Antigravity. I'll also share the three common mistakes that burned us early on.
What Context Caching Is (and Why It Actually Matters in 2026)
Gemini API context caching lets you store content — system prompts, documents, code files, images — on Google's servers and reference that stored content in subsequent API calls. Instead of sending 50,000 tokens on every request, you send a cache reference token and pay a fraction of the normal input price.
The economics only work above a certain request volume, but for production systems the savings are significant.
Current pricing for Gemini 2.5 Pro (as of May 2026):
| Operation | Price per 1M tokens |
|---|---|
| Normal input | $1.25 |
| Cache creation | $1.5625 (1.25×) |
| Cache storage | $0.04375/hour |
| Cache reads | $0.3125 (25% of normal) |
The break-even point depends on your usage. Here's the formula:
# Break-even calculation
fixed_cost = (cache_tokens / 1M) × $1.5625 # creation
+ (cache_tokens / 1M) × $0.04375 × ttl_hours # storage
savings_per_request = (cache_tokens / 1M) × ($1.25 - $0.3125)
break_even_requests = fixed_cost / savings_per_request
For 45,000 tokens with a 1-hour TTL:
fixed_cost = 0.045 × $1.5625 + 0.045 × $0.04375 = $0.0723
savings_per_request = 0.045 × $0.9375 = $0.0422
break_even = $0.0723 / $0.0422 ≈ 1.71 requests
If you make more than 2 requests per hour using the same prompt, caching saves money. In our case we're at 300 requests per day.
Understanding the Constraints Before You Start
The most important constraint is the minimum token requirement: 32,768 tokens for Gemini 2.5 Pro. Attempts to cache content below this threshold return an INVALID_ARGUMENT error. This trips up almost every developer who tries caching for the first time.
Other constraints:
- Cache and request must use the same model (including version)
- Default TTL is 60 minutes, maximum is 24 hours
- Cached content counts toward your quota limits
Keep these in mind while designing your caching strategy.
Core Implementation in TypeScript
These are the classes we run in production. They handle cache creation, cache-aware queries, automatic renewal, and retry logic for cache invalidation errors.
Cache Manager
// src/cache-manager.ts
import { GoogleGenAI } from "@google/genai";
import * as fs from "fs/promises";
const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY! });
export interface CacheResult {
cacheName: string;
expiresAt: Date;
tokenCount: number;
}
/**
* Creates a text cache from a content string.
* Validates token count before attempting creation.
*/
export async function createTextCache(
content: string,
model: string = "gemini-2.5-pro",
ttlSeconds: number = 3600
): Promise<CacheResult> {
// Validate token count — this will throw before hitting the API
const countResult = await ai.models.countTokens({
model,
contents: [{ role: "user", parts: [{ text: content }] }],
});
const tokenCount = countResult.totalTokens ?? 0;
if (tokenCount < 32768) {
throw new Error(
`Content too small for caching: ${tokenCount} tokens. ` +
`Gemini 2.5 Pro requires at least 32,768 tokens.`
);
}
console.log(`Creating cache for ${tokenCount.toLocaleString()} tokens...`);
const cache = await ai.caches.create({
model,
contents: [{ role: "user", parts: [{ text: content }] }],
ttl: `${ttlSeconds}s`,
});
const expiresAt = new Date(Date.now() + ttlSeconds * 1000);
console.log(`Cache created: ${cache.name} (expires ${expiresAt.toISOString()})`);
return {
cacheName: cache.name ?? "",
expiresAt,
tokenCount,
};
}
/**
* Creates a cache from multiple source files with a system instruction.
* Useful for codebase analysis tools.
*/
export async function createFileCache(
filePaths: string[],
systemInstruction: string,
model: string = "gemini-2.5-pro",
ttlSeconds: number = 3600
): Promise<CacheResult> {
const parts = await Promise.all(
filePaths.map(async (filePath) => {
const content = await fs.readFile(filePath, "utf-8");
const ext = filePath.split(".").pop()?.toLowerCase() ?? "text";
return { text: `\`\`\`${ext}\n// File: ${filePath}\n${content}\n\`\`\`` };
})
);
const cache = await ai.caches.create({
model,
systemInstruction: { parts: [{ text: systemInstruction }] },
contents: [{ role: "user", parts }],
ttl: `${ttlSeconds}s`,
});
return {
cacheName: cache.name ?? "",
expiresAt: new Date(Date.now() + ttlSeconds * 1000),
tokenCount: 0,
};
}Cache-Aware Query Function
// src/cached-query.ts
import { GoogleGenAI } from "@google/genai";
const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY! });
export interface QueryResult {
text: string;
inputTokens: number;
outputTokens: number;
cachedTokens: number;
costSavedUSD: number;
}
/**
* Queries the model using a cached context.
* Reports cache hit tokens and estimated cost savings.
*/
export async function queryWithCache(
cacheName: string,
userMessage: string,
model: string = "gemini-2.5-pro"
): Promise<QueryResult> {
const response = await ai.models.generateContent({
model,
cachedContent: cacheName,
contents: [{ role: "user", parts: [{ text: userMessage }] }],
});
const usage = response.usageMetadata;
const inputTokens = usage?.promptTokenCount ?? 0;
const outputTokens = usage?.candidatesTokenCount ?? 0;
const cachedTokens = usage?.cachedContentTokenCount ?? 0;
// Cost delta: normal input ($1.25/1M) minus cache read ($0.3125/1M)
const costSavedUSD = (cachedTokens * 0.9375) / 1_000_000;
return {
text: response.text ?? "",
inputTokens,
outputTokens,
cachedTokens,
costSavedUSD,
};
}Production-Ready Client with Auto-Renewal
// src/cache-aware-client.ts
import { createTextCache } from "./cache-manager";
import { queryWithCache } from "./cached-query";
/**
* Wraps a large prompt with automatic cache creation and renewal.
* Handles cache expiry, invalidation errors, and retry logic.
*/
export class CacheAwareGeminiClient {
private cacheName: string | null = null;
private cacheExpiresAt: Date | null = null;
private readonly content: string;
private readonly model: string;
private readonly ttlSeconds: number;
private readonly renewBufferSeconds = 300; // renew 5 min before expiry
constructor(
content: string,
model: string = "gemini-2.5-pro",
ttlSeconds: number = 3600
) {
this.content = content;
this.model = model;
this.ttlSeconds = ttlSeconds;
}
private needsRenewal(): boolean {
if (!this.cacheExpiresAt || !this.cacheName) return true;
const bufferMs = this.renewBufferSeconds * 1000;
return Date.now() + bufferMs >= this.cacheExpiresAt.getTime();
}
private async ensureValidCache(): Promise<string> {
if (!this.needsRenewal()) return this.cacheName!;
const result = await createTextCache(
this.content,
this.model,
this.ttlSeconds
);
this.cacheName = result.cacheName;
this.cacheExpiresAt = result.expiresAt;
return this.cacheName;
}
async query(userMessage: string): Promise<QueryResult> {
const MAX_RETRIES = 3;
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
try {
const cacheName = await this.ensureValidCache();
const result = await queryWithCache(cacheName, userMessage, this.model);
if (result.cachedTokens > 0) {
console.log(
`Cache hit: ${result.cachedTokens.toLocaleString()} tokens ` +
`(saved $${result.costSavedUSD.toFixed(6)})`
);
}
return result;
} catch (error) {
const err = error as Error;
const isInvalidCache =
err.message.includes("NOT_FOUND") ||
err.message.includes("INVALID_ARGUMENT");
if (isInvalidCache && attempt < MAX_RETRIES) {
console.warn(`Cache error on attempt ${attempt}, resetting cache...`);
this.cacheName = null;
this.cacheExpiresAt = null;
} else {
throw err;
}
}
}
throw new Error("Cache retry limit reached");
}
}Three Production Patterns That Actually Work
Pattern 1: System Prompt Caching (Highest ROI)
If you have a long system prompt — with company policies, product documentation, or a persona definition — this is where caching delivers the fastest payoff.
// Customer support bot with cached documentation
const SUPPORT_DOCS = `
You are a support agent for [Company Name].
[Product Manual - ~12,000 words]
[Return Policy Documentation - ~4,000 words]
[FAQ Collection - ~8,000 words]
[Escalation Procedures - ~3,000 words]
// Total: ~40,000+ tokens (well above the 32,768 minimum)
`;
const client = new CacheAwareGeminiClient(
SUPPORT_DOCS,
"gemini-2.5-pro",
7200 // 2-hour cache, renews every 2 hours
);
// All subsequent queries reference the cache
const answer = await client.query("What is your return policy for digital products?");When to use: System prompts above 33K tokens. If yours is smaller, consolidate documentation until you hit the threshold.
Pattern 2: Codebase Caching for Dev Tools
For code review agents, documentation generators, or refactoring tools — cache the source files once and run multiple analysis passes cheaply.
// Cache the codebase at CI start, run multiple checks
async function runCodeAnalysisPipeline(sourceFiles: string[]) {
const systemInstruction = `
You are a senior TypeScript engineer reviewing production code.
Analyze the provided files for:
- Security vulnerabilities
- Performance bottlenecks
- Type safety issues
- Missing error handling
- Opportunities for abstraction
`;
// Cache once at the start of the pipeline
const cache = await createFileCache(
sourceFiles,
systemInstruction,
"gemini-2.5-pro",
86400 // 24-hour cache for CI/CD pipelines
);
// Run multiple analyses against the same cached codebase
const checks = [
"List all security vulnerabilities in order of severity",
"Identify functions with O(n²) or worse complexity",
"Find all places where errors are silently swallowed",
"Suggest which modules would benefit most from unit tests",
];
const results = await Promise.all(
checks.map((check) =>
queryWithCache(cache.cacheName, check, "gemini-2.5-pro")
)
);
const totalSaved = results.reduce((sum, r) => sum + r.costSavedUSD, 0);
console.log(`Analysis complete. Total saved: $${totalSaved.toFixed(4)}`);
return results.map((r, i) => ({ check: checks[i], result: r.text }));
}Pattern 3: Document Q&A Session Caching
For RAG-based document analysis, cache the document content at session start and handle multiple user questions without re-uploading the document each time.
// Document Q&A session with cached content
async function documentSession(documentContent: string, questions: string[]) {
// Validate and cache the document
const canCache = await ai.models.countTokens({
model: "gemini-2.5-pro",
contents: [{ role: "user", parts: [{ text: documentContent }] }],
}).then((r) => (r.totalTokens ?? 0) >= 32768);
if (!canCache) {
throw new Error("Document is too short for context caching");
}
const cache = await createTextCache(
`Answer questions based only on this document:\n\n${documentContent}`,
"gemini-2.5-pro",
3600
);
console.log(`Document cached. Answering ${questions.length} questions...`);
let totalSaved = 0;
for (const question of questions) {
const result = await queryWithCache(cache.cacheName, question);
totalSaved += result.costSavedUSD;
console.log(`Q: ${question}`);
console.log(`A: ${result.text.substring(0, 150)}...`);
console.log(` Tokens cached: ${result.cachedTokens.toLocaleString()}\n`);
}
console.log(`Session complete. Total saved: $${totalSaved.toFixed(4)}`);
}Python Implementation with Prometheus Monitoring
For teams using Python or wanting observability into cache performance:
# src/gemini_cache_client.py
import time
from google.genai import Client
from prometheus_client import Counter, Histogram, Gauge
client = Client(api_key="YOUR_GEMINI_API_KEY")
# Prometheus metrics
cache_hits_total = Counter(
'gemini_cache_hits_total',
'Number of requests that hit the context cache'
)
cache_misses_total = Counter(
'gemini_cache_misses_total',
'Number of requests without a cache hit'
)
cost_saved_usd_total = Counter(
'gemini_cost_saved_usd_total',
'Cumulative cost savings from context caching in USD'
)
cached_tokens_histogram = Histogram(
'gemini_cached_tokens_per_request',
'Distribution of cached token counts per request',
buckets=[0, 1000, 5000, 10000, 32768, 50000, 100000]
)
cache_hit_rate = Gauge(
'gemini_cache_hit_rate',
'Rolling cache hit rate (last 100 requests)'
)
def query_with_cache_monitored(
cache_name: str,
user_message: str,
model: str = "gemini-2.5-pro"
) -> str:
"""
Query Gemini using a cached context and record Prometheus metrics.
Falls back gracefully if the cache is expired or invalid.
"""
try:
response = client.models.generate_content(
model=model,
cached_content=cache_name,
contents=[{"role": "user", "parts": [{"text": user_message}]}]
)
usage = response.usage_metadata
cached_token_count = getattr(usage, 'cached_content_token_count', 0) or 0
if cached_token_count > 0:
cache_hits_total.inc()
saved = cached_token_count * 0.9375 / 1_000_000 # USD
cost_saved_usd_total.inc(saved)
cached_tokens_histogram.observe(cached_token_count)
else:
cache_misses_total.inc()
return response.text
except Exception as e:
if "NOT_FOUND" in str(e) or "INVALID_ARGUMENT" in str(e):
# Cache expired — caller should recreate and retry
raise CacheExpiredError(f"Cache {cache_name} is no longer valid") from e
raise
class CacheExpiredError(Exception):
"""Raised when the referenced cache has expired or been invalidated."""
passReal Cost Comparison: Before and After
Here's the actual before/after for our system (300 requests/day, 45,000-token system prompt):
Before caching:
Monthly input tokens: 45,200 × 300 × 30 = 406,800,000
Monthly output tokens: 500 × 300 × 30 = 4,500,000
Input cost: 406.8M × $1.25/1M = $508.50
Output cost: 4.5M × $5.00/1M = $22.50
Monthly total: $531.00
After caching (1-hour TTL, 24 refreshes/day):
Cache creation: 720 × 0.045M × $1.5625/1M = $50.63
Cache storage: 720h × 0.045M × $0.04375/1M = $1.42
User input only: 1.8M × $1.25/1M = $2.25
Cache reads: 405M × $0.3125/1M = $126.56
Output (unchanged): $22.50
Monthly total: $203.36
Result: 62% reduction ($531 → $203)
After tuning TTL based on actual traffic patterns (longer cache on weekends when volume drops), we reached 68% savings month-over-month.
The Three Traps That Catch Everyone
Trap 1: Token count below the minimum
This is by far the most common failure. If your system prompt is under 32,768 tokens, the API returns INVALID_ARGUMENT with an unhelpful error message.
// Always check before attempting cache creation
async function canUseContextCache(
content: string,
model: string = "gemini-2.5-pro"
): Promise<{ cacheable: boolean; tokenCount: number }> {
const result = await ai.models.countTokens({
model,
contents: [{ role: "user", parts: [{ text: content }] }],
});
const tokenCount = result.totalTokens ?? 0;
return { cacheable: tokenCount >= 32768, tokenCount };
}
// Quick check in Antigravity terminal:
// GEMINI_API_KEY=... node -e "
// require('./src/cached-query').canUseContextCache(yourContent)
// .then(r => console.log(r))
// "Trap 2: Model version mismatch
The model name must match exactly between cache creation and cache reference. gemini-2.5-pro and gemini-2.5-pro-latest are treated as different models.
// Define your model as a constant and use it everywhere
const GEMINI_MODEL = "gemini-2.5-pro-preview-05-06" as const;
// ❌ This will throw NOT_FOUND
const cache = await createTextCache(content, "gemini-2.5-pro");
const response = await queryWithCache(cacheName, message, "gemini-2.5-pro-latest");
// ✅ Use the same constant throughout
const cache = await createTextCache(content, GEMINI_MODEL);
const response = await queryWithCache(cacheName, message, GEMINI_MODEL);Trap 3: Forgetting that storage costs accumulate
A 24-hour TTL on 100K tokens adds up. Always set TTL to match your actual usage window.
// Match TTL to your traffic pattern
const TTL_CONFIG = {
businessHours: 43_200, // 12h — content changes at day boundaries
userSession: 3_600, // 1h — per-user document analysis
ciPipeline: 86_400, // 24h — rebuild once per day at most
realtime: 1_800, // 30min — short-lived context
} as const;
// Check remaining TTL before deciding to renew
async function getCacheStatus(cacheName: string) {
const cache = await ai.caches.get({ name: cacheName });
const expiresAt = new Date(cache.expireTime as string);
const remainingSeconds = (expiresAt.getTime() - Date.now()) / 1000;
return {
valid: remainingSeconds > 0,
remainingSeconds,
expiresAt,
};
}Putting It All Together
Context caching is one of those optimizations where the cost/benefit is immediately clear once you run the numbers. The implementation isn't complex, but getting the details right matters.
The first thing to do today: run countTokens on your current system prompt and see where you stand. If you're above 33K tokens, you can be saving money by this afternoon.
# Quick check from Antigravity's terminal
node -e "
const { GoogleGenAI } = require('@google/genai');
const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY });
const prompt = require('fs').readFileSync('./your-system-prompt.txt', 'utf-8');
ai.models.countTokens({
model: 'gemini-2.5-pro',
contents: [{ role: 'user', parts: [{ text: prompt }] }]
}).then(r => {
const tokens = r.totalTokens;
const cacheable = tokens >= 32768;
console.log(\`Tokens: \${tokens.toLocaleString()}\`);
console.log(cacheable ? '✅ Cacheable' : '❌ Below minimum (32,768 required)');
if (cacheable) {
const monthly_savings = tokens * 0.9375 / 1e6 * 300 * 30;
console.log(\`Estimated monthly savings (300 req/day): \$\${monthly_savings.toFixed(2)}\`);
}
});
"For systems at scale, context caching isn't optional — it's how you keep API costs from eating your margins as traffic grows.
Advanced: Combining Context Caching with Streaming
One pattern worth covering separately: context caching works seamlessly with streaming responses. For user-facing applications where response latency matters, you can combine both features.
// src/streaming-cached-client.ts
import { GoogleGenAI } from "@google/genai";
const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY! });
/**
* Stream a response using cached context.
* Useful for chat interfaces where you want real-time output
* while still benefiting from caching.
*/
async function streamWithCache(
cacheName: string,
userMessage: string,
onChunk: (text: string) => void,
model: string = "gemini-2.5-pro"
): Promise<{ totalCachedTokens: number; totalOutputTokens: number }> {
const stream = await ai.models.generateContentStream({
model,
cachedContent: cacheName,
contents: [{ role: "user", parts: [{ text: userMessage }] }],
});
let totalCachedTokens = 0;
let totalOutputTokens = 0;
let fullText = "";
for await (const chunk of stream) {
const chunkText = chunk.text ?? "";
if (chunkText) {
onChunk(chunkText);
fullText += chunkText;
}
// Capture usage metadata from the final chunk
const usage = chunk.usageMetadata;
if (usage) {
totalCachedTokens = usage.cachedContentTokenCount ?? 0;
totalOutputTokens = usage.candidatesTokenCount ?? 0;
}
}
return { totalCachedTokens, totalOutputTokens };
}
// Example usage in an Express.js route
// app.post('/chat', async (req, res) => {
// res.setHeader('Content-Type', 'text/event-stream');
// res.setHeader('Cache-Control', 'no-cache');
//
// const { totalCachedTokens } = await streamWithCache(
// process.env.SYSTEM_CACHE_NAME!,
// req.body.message,
// (chunk) => res.write(`data: ${JSON.stringify({ chunk })}\n\n`)
// );
//
// res.write(`data: ${JSON.stringify({ done: true, cachedTokens: totalCachedTokens })}\n\n`);
// res.end();
// });The streaming + caching combination gives you the best of both worlds: fast first-token latency from the streaming API, and reduced costs from cached context. In our chat application, this pattern handles 80% of all message volume.
Cache Invalidation Strategy: When to Recreate vs. Extend
One question that comes up in production: when your source content changes, should you invalidate the cache, extend its TTL, or let it expire naturally?
The answer depends on how frequently your content changes and how sensitive users are to seeing slightly stale information.
// src/cache-lifecycle.ts
import { GoogleGenAI } from "@google/genai";
import { createTextCache, CacheResult } from "./cache-manager";
const ai = new GoogleGenAI({ apiKey: process.env.GEMINI_API_KEY! });
interface CacheLifecycleManager {
cacheName: string;
contentHash: string;
expiresAt: Date;
}
function hashContent(content: string): string {
// Simple hash for change detection — use crypto.createHash in production
return Buffer.from(content).toString("base64").slice(0, 16);
}
/**
* Smart cache manager that detects content changes
* and only recreates the cache when necessary.
*/
export class SmartCacheManager {
private state: CacheLifecycleManager | null = null;
private content: string;
private model: string;
private ttlSeconds: number;
constructor(
initialContent: string,
model: string = "gemini-2.5-pro",
ttlSeconds: number = 3600
) {
this.content = initialContent;
this.model = model;
this.ttlSeconds = ttlSeconds;
}
updateContent(newContent: string): void {
this.content = newContent;
// Content change detected — cache will be recreated on next query
}
async getCacheName(): Promise<string> {
const contentHash = hashContent(this.content);
const isExpired = !this.state || Date.now() >= this.state.expiresAt.getTime();
const isStale = this.state?.contentHash !== contentHash;
if (isExpired || isStale) {
if (isStale && this.state) {
console.log("Content changed — recreating cache");
// Delete old cache to avoid storage charges
try {
await ai.caches.delete({ name: this.state.cacheName });
} catch {
// Cache may have already expired — safe to ignore
}
}
const result = await createTextCache(
this.content,
this.model,
this.ttlSeconds
);
this.state = {
cacheName: result.cacheName,
contentHash,
expiresAt: result.expiresAt,
};
}
return this.state!.cacheName;
}
}When to use each approach:
- Let expire naturally: When content changes infrequently (daily or less) and brief stale periods are acceptable
- Proactive invalidation + recreation: When content changes frequently and accuracy is critical (real-time data, frequently updated documents)
- Extend TTL: Use
ai.caches.update()to extend a valid cache before it expires if recreation would be expensive
// Extend TTL without recreating the cache
async function extendCacheTTL(cacheName: string, additionalSeconds: number) {
const cache = await ai.caches.get({ name: cacheName });
const currentExpiry = new Date(cache.expireTime as string);
const newTTL = Math.ceil(
(currentExpiry.getTime() - Date.now()) / 1000 + additionalSeconds
);
await ai.caches.update({
name: cacheName,
config: { ttl: `${newTTL}s` },
});
console.log(`Extended cache TTL by ${additionalSeconds}s`);
}Cost Optimization Decision Tree
Use this framework to decide whether context caching is worth implementing for any given use case:
Is the repeated content larger than 32,768 tokens?
├─ No → Context caching is not available. Consider prompt compression instead.
└─ Yes ↓
How many requests per TTL period reference this content?
├─ < 2 requests → Caching costs more than it saves. Skip it.
├─ 2–5 requests → Marginal benefit. Worth it only if content is very large (100K+).
└─ > 5 requests → Strong candidate for caching. Proceed. ↓
How often does the content change?
├─ Changes each request → Cache provides no benefit (content is dynamic).
├─ Changes hourly → Set TTL to 60 minutes. Recreate on change.
├─ Changes daily → Set TTL to 24 hours. Recreate after content update.
└─ Static → Set TTL to 24 hours. Cache indefinitely with renewal logic.
Expected monthly savings > storage cost overhead?
├─ Calculate: (request_count × cached_tokens × $0.9375/1M) - (creation_cost + storage_cost)
└─ If positive → Implement caching. If negative → Review your TTL or skip.
Running this decision tree against your actual usage numbers prevents over-engineering (caching everything) or under-engineering (missing obvious opportunities).
Integrating with Antigravity's Workflow
One reason I build these systems within Antigravity rather than in a standalone terminal: Antigravity's multi-file context awareness lets you implement caching logic across files without losing the thread.
A typical Antigravity session for setting up context caching might look like:
- Open the project in Antigravity — it indexes your existing API code automatically
- Ask for a cost analysis: "Analyze our API call patterns in
src/api/and identify where we're repeatedly sending the same large content" - Let Antigravity trace the call graph — it will find the system prompt construction code and estimate token counts
- Request the implementation: "Add context caching to these three call sites, with automatic renewal and proper error handling"
- Review and test — use Antigravity's diff view to inspect changes before committing
The multi-file editing capability is particularly useful here because cache management typically spans three or four files: the cache manager, the API client, environment configuration, and tests. Antigravity can edit all of them in a single operation while keeping cross-file references consistent.
Next Steps
Context caching is one layer in a broader cost optimization strategy. Once you have caching working, the next two optimizations to consider are:
- Model tiering: Route simpler queries to Gemini Flash (significantly cheaper) and reserve Gemini Pro for complex reasoning tasks. Combined with caching, this can reduce costs by an additional 30–50%.
- Batch processing: For offline workloads that don't need real-time responses, the Gemini Batch API offers further discounts. Combine with caching for maximum savings on large-scale document processing.
The combination of these three techniques — context caching, model tiering, and batch processing — can bring the cost of a production AI system down to a fraction of the naive implementation. Start with caching today and build from there.
A Note on the Evolving Pricing Landscape
The numbers in this article reflect Gemini 2.5 Pro pricing as of May 2026. Google has adjusted context caching pricing several times since the feature launched, generally in the direction of lower costs as the infrastructure matures. Check the official Gemini API pricing page before making business decisions based on the figures here — the formula is stable even when specific numbers shift.
What won't change is the underlying principle: if you repeatedly send the same large block of content, caching almost always saves money once you exceed a handful of requests per hour. The break-even calculation at the top of this article stays valid regardless of where Google sets the specific per-token rates.
One thing I find genuinely encouraging about context caching: it rewards good system design. Applications that have a clear separation between static context (instructions, knowledge base, documentation) and dynamic input (user messages, live data) are easy to cache. Systems that construct a new system prompt for every request — mixing static and dynamic content together — tend to be both more expensive and harder to optimize. Caching provides a financial incentive to build clean AI application architecture, which feels like the right direction for the ecosystem.
If you implement this and the numbers surprise you in either direction, I'd be curious to hear about it. The community data on real-world caching performance is still thin compared to the theoretical pricing, and more datapoints help everyone calibrate expectations.