GEMINI LABJP
OMNI — Gemini Omni Flash, a natively multimodal model, enters API public preview for building custom video workflowsNANO — Nano Banana 2 Lite arrives as the fastest and most cost-efficient Gemini image model yetFLASH — Gemini 3.5 Flash reaches general availability with sustained frontier performance on agentic and coding tasksAGENTS — Managed Agents enter public preview in the Gemini API, running autonomous, stateful agents in isolated Google-hosted Linux sandboxesMEMORY — The Memory Bank IngestEvents API is GA, decoupling event ingestion from memory generation for continuous streamingTHROUGHPUT — Provisioned throughput now accepts up to seven pending model orders for the same model and regionOMNI — Gemini Omni Flash, a natively multimodal model, enters API public preview for building custom video workflowsNANO — Nano Banana 2 Lite arrives as the fastest and most cost-efficient Gemini image model yetFLASH — Gemini 3.5 Flash reaches general availability with sustained frontier performance on agentic and coding tasksAGENTS — Managed Agents enter public preview in the Gemini API, running autonomous, stateful agents in isolated Google-hosted Linux sandboxesMEMORY — The Memory Bank IngestEvents API is GA, decoupling event ingestion from memory generation for continuous streamingTHROUGHPUT — Provisioned throughput now accepts up to seven pending model orders for the same model and region
Articles/Dev Tools
Dev Tools/2026-07-11Advanced

Never Generate the Same Narration Twice: Cache-Key Design and Invalidation for Gemini TTS Output

For apps that replay the same audio over and over—meditation, language learning, storytelling—caching the Gemini TTS output itself drives the variable cost to near zero. This is a working design: how to build a cache key that text alone can't cover, a two-tier server-plus-device cache, and an invalidation policy that survives model shutdowns like the August 17 image-model deprecation.

gemini-api270tts7cache2cost-optimization30mobile-app3

Premium Article

I run a small meditation-narration app as an indie developer. One morning I was scanning my Gemini TTS bill, and I froze.

The same audio was being generated again and again.

Every time a user played "Three Minutes Before Sleep," the app sent the exact same script to TTS and produced the exact same audio from scratch. The script was fixed. The voice was fixed. Yet every playback added to the bill. In hindsight it was an obvious mistake, but because it worked, I was slow to notice.

That was when something clicked. Unlike a dynamic conversational agent, most audio in healing or learning apps is "the same script, listened to repeatedly." So save the generated audio itself, and stop rebuilding it from the second play onward. That single change drops the bill dramatically.

The hard part was designing the key that makes saving safe, and handling invalidation when the model or the voice changes. Get this wrong and you either serve old audio after you meant to change the voice, or the cache misses entirely and the bill comes back.

Grasp the order of magnitude first

Before any fix, it helps to grasp the cost of doing nothing. Without that, you can't judge how much effort is worth spending.

TTS is usually billed by the character count of input text. As an illustrative rate for explanation, I'll use ¥0.02 per character (your actual rate depends on your model and contract, so always substitute your own bill).

Here are my app's numbers.

ItemValue
Number of narrations30
Script per narration~1,500 characters
Active users8,000 DAU
Plays per user6 per day

Generating on every play means 8,000 × 6 × 1,500 = 72 million characters per day. At ¥0.02, that is ¥1,440,000 per day. Not a number anyone can pay.

Generating the whole library once is 30 × 1,500 = 45,000 characters, or ¥900. Unless you rewrite a script, that ¥900 never recurs.

The same audio, delivered for ¥1,440,000/day versus ¥900 once. That gap is the entire reason to invest in caching.

Text alone is not enough for a cache key

My first near-mistake was to key on the raw script text. That fails in production every time.

The same text becomes different audio if you change the voice. Swapping the model, changing the speaking rate, or switching the output format from mp3 to wav all change the bytes. A text-only key confuses them. The bug "I switched the voice from female to male, but the old female voice keeps coming back" was caused by exactly this.

The cache key must include every element that determines the identity of the audio.

  1. Normalized script text (trim whitespace, unify line endings)
  2. Voice name
  3. Model ID (e.g., gemini-3.1-flash-tts)
  4. Output format (mp3 / wav / opus)
  5. Generation parameters such as speaking rate

Order these canonically, then hash.

import { createHash } from "node:crypto";
 
interface TtsRequest {
  text: string;
  voice: string;
  model: string;
  format: "mp3" | "wav" | "opus";
  speakingRate: number;
}
 
// Key only on the elements that determine audio identity, in a fixed order.
export function buildCacheKey(req: TtsRequest): string {
  const normalizedText = req.text
    .replace(/\r\n/g, "\n")
    .replace(/\s+$/gm, "")
    .trim();
 
  const canonical = JSON.stringify({
    t: normalizedText,
    v: req.voice,
    m: req.model,
    f: req.format,
    r: req.speakingRate,
  });
 
  return createHash("sha256").update(canonical).digest("hex");
}

The critical point is including the model ID. Omit it and the model-swap invalidation described below stops working entirely. When I'm unsure what belongs in the key, I default to "include everything that could change the audio bytes."

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
Build a SHA-256 cache key that includes text, voice, model ID, format, and speaking rate, so changing the voice never returns stale audio
Use a two-tier server-plus-device cache to keep daily regeneration near zero even at 8,000 DAU
Adopt a freeze-or-regenerate invalidation policy so a model shutdown like the August 17 deprecation never leaves your users in silence
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

Dev Tools2026-03-31
Gemini API × React Native Operational Notes — Indie Mobile App in Production
Operational notes from running Gemini API inside a React Native/Expo indie mobile app on iOS and Android: real device pitfalls, AdMob coexistence, Cold Start mitigation, AsyncStorage TTL design, and cost realities at 65,000 monthly requests
API / SDK2026-05-29
Layering Gemini API Response Caches in Three Tiers — How I Split Memory, Redis, and Context Cache
Notes from running a three-tier cache (in-memory, Redis, Gemini Context Cache) in front of the Gemini API for six weeks across a wallpaper app — actual hit rates, billing impact, and the invalidation traps that ate me alive.
Dev Tools2026-06-30
Building an AI-Powered Content Site with Gemini API and Astro
Combine Astro Server Endpoints and Content Collections with the Gemini API to add AI summaries, related-article recommendations, and auto-tagging.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →