| name | gemini-api-caching |
| description | Best practices for Gemini API caching strategy including cache versioning, entity-stable keys, and cache busting for WescoBar |
Gemini API Caching
Purpose
Implement robust caching strategies for Gemini API calls to reduce cost, improve latency, and provide better user experience while supporting cache invalidation when needed.
When to Use
- Implementing any Gemini API feature
- Generating character portraits or world images
- Caching AI-generated content
- Reducing API quota usage
- Improving app performance
Caching Strategy
From AGENTS.md Section 3: "A robust, multi-layered local storage cache"
1. Cache Versioning
2. Entity-Stable Keys
3. Cache Busting
Instructions
Step 1: Define Cache Version
const CACHE_VERSION = 'v2';
const getCacheKey = (entity: string, id: string) => {
return `${CACHE_VERSION}-${entity}:${id}`;
};
const portraitKey = getCacheKey('character-portrait', 'core-pablo');
Purpose: Instant global cache invalidation by bumping version
When to bump:
- Prompt template changes
- Image generation model updates
- Quality improvements needed
- Global regeneration required
Step 2: Use Entity-Stable Keys
const cacheKey = `${CACHE_VERSION}-character-portrait:${character.id}`;
const cacheKey = `${CACHE_VERSION}-${fullPromptText}`;
Why entity-stable:
- Same character = same cache key
- Minor prompt tweaks don't invalidate cache
- Consistent across sessions
- Reduces unnecessary regeneration
Entity Types:
character-portrait:<id> - Character portraits
world-scene:<id> - World/location images
story-illustration:<id> - Story illustrations
ui-element:<name> - UI-generated images
Step 3: Implement Cache Read
async function getCharacterPortrait(
character: Character,
options?: { forceRebuild?: boolean }
): Promise<string> {
if (options?.forceRebuild) {
return await generateNewPortrait(character);
}
const cacheKey = `${CACHE_VERSION}-character-portrait:${character.id}`;
const cached = localStorage.getItem(cacheKey);
if (cached) {
console.log(`✅ Cache hit: ${cacheKey}`);
return cached;
}
console.log(`⚠️ Cache miss: ${cacheKey}`);
const imageUrl = await generateNewPortrait(character);
localStorage.setItem(cacheKey, imageUrl);
return imageUrl;
}
Step 4: Implement Cache Write
async function generateAndCachePortrait(
character: Character
): Promise<string> {
const imageUrl = await geminiService.generateImage({
prompt: buildCharacterPrompt(character),
});
const cacheKey = `${CACHE_VERSION}-character-portrait:${character.id}`;
localStorage.setItem(cacheKey, imageUrl);
console.log(`💾 Cached: ${cacheKey}`);
return imageUrl;
}
Step 5: Implement Cache Busting
async function regeneratePortrait(character: Character) {
const imageUrl = await getCharacterPortrait(character, {
forceRebuild: true
});
return imageUrl;
}
function clearPortraitCache(character: Character) {
const cacheKey = `${CACHE_VERSION}-character-portrait:${character.id}`;
localStorage.removeItem(cacheKey);
console.log(`🗑️ Cleared cache: ${cacheKey}`);
}
function clearAllPortraits() {
const prefix = `${CACHE_VERSION}-character-portrait:`;
Object.keys(localStorage)
.filter(key => key.startsWith(prefix))
.forEach(key => localStorage.removeItem(key));
console.log(`🗑️ Cleared all portrait cache`);
}
Step 6: Handle Cache Expiration (Optional)
interface CachedImage {
url: string;
timestamp: number;
version: string;
}
function getCachedImage(key: string, maxAge: number = 7 * 24 * 60 * 60 * 1000): string | null {
const cached = localStorage.getItem(key);
if (!cached) return null;
try {
const data: CachedImage = JSON.parse(cached);
if (data.version !== CACHE_VERSION) {
localStorage.removeItem(key);
return null;
}
const age = Date.now() - data.timestamp;
if (age > maxAge) {
localStorage.removeItem(key);
return null;
}
return data.url;
} catch {
localStorage.removeItem(key);
return null;
}
}
function setCachedImage(key: string, url: string) {
const data: CachedImage = {
url,
timestamp: Date.now(),
version: CACHE_VERSION
};
localStorage.setItem(key, JSON.stringify(data));
}
Integration Patterns
With Rate Limiting Skill
Combine with gemini-api-rate-limiting skill:
async function generateImagesSequentially(characters: Character[]) {
for (const character of characters) {
const cached = await getCharacterPortrait(character);
if (cached) {
updateCharacterImage(character.id, cached);
continue;
}
try {
const imageUrl = await generateWithTimeout(character, 30000);
updateCharacterImage(character.id, imageUrl);
} catch (error) {
handleGenerationError(character.id, error);
}
await new Promise(resolve => setTimeout(resolve, 2000));
}
}
UI Integration
function CharacterCard({ character }: { character: Character }) {
const [imageUrl, setImageUrl] = useState<string | null>(null);
const [isRegenerating, setIsRegenerating] = useState(false);
useEffect(() => {
getCharacterPortrait(character).then(setImageUrl);
}, [character.id]);
const handleRegenerate = async () => {
setIsRegenerating(true);
const newUrl = await regeneratePortrait(character);
setImageUrl(newUrl);
setIsRegenerating(false);
};
return (
<div>
<img src={imageUrl || placeholderImage} alt={character.name} />
<button onClick={handleRegenerate} disabled={isRegenerating}>
{isRegenerating ? 'Regenerating...' : 'Regenerate Portrait'}
</button>
</div>
);
}
Cache Management UI
function CacheManagement() {
const [stats, setStats] = useState({ portraits: 0, scenes: 0, total: 0 });
useEffect(() => {
const portraitKeys = Object.keys(localStorage)
.filter(key => key.includes('character-portrait'));
setStats({
portraits: portraitKeys.length,
scenes: 0,
total: localStorage.length
});
}, []);
const clearAllCache = () => {
const confirmed = window.confirm('Clear all cached images? This will regenerate on next load.');
if (confirmed) {
Object.keys(localStorage)
.filter(key => key.startsWith(CACHE_VERSION))
.forEach(key => localStorage.removeItem(key));
alert('Cache cleared!');
window.location.reload();
}
};
return (
<div>
<h3>Gemini API Cache</h3>
<p>Cached portraits: {stats.portraits}</p>
<p>Total cached items: {stats.total}</p>
<button onClick={clearAllCache}>Clear All Cache</button>
</div>
);
}
Best Practices
- Always check cache first - Reduce API calls
- Use entity-stable keys - Don't invalidate on prompt changes
- Version globally - Easy bulk invalidation
- Provide regenerate option - Let users force refresh
- Consider expiration - For time-sensitive content
- Monitor cache size - localStorage has limits (~5-10MB)
- Handle cache errors - Fallback to generation
Related Skills
gemini-api-rate-limiting - Combine cache with rate limiting
gemini-api-error-handling - Handle cache failures
Storage Limits
localStorage Limits
- Size: ~5-10MB depending on browser
- Monitor usage: Track total cache size
- Cleanup strategy: Remove oldest entries when full
function getCacheSize(): number {
let total = 0;
for (const key in localStorage) {
if (localStorage.hasOwnProperty(key)) {
total += localStorage[key].length + key.length;
}
}
return total;
}
if (getCacheSize() > 5 * 1024 * 1024) {
clearOldestEntries();
}
Notes
- Caching reduces API costs significantly
- Entity-stable keys prevent unnecessary cache invalidation
- Global version bumping enables instant cache refresh
- Force rebuild option gives users control
- Combine with rate limiting for complete API management