●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
Firebase Genkit × Gemini API in Production — Field Notes from an Indie Developer Running 50M-Download Apps
Production field notes from running Firebase Genkit and Gemini API on the back end of indie wallpaper and mindfulness apps that cumulatively passed 50M downloads. Covers Flow and Tool design, RAG, deployment, real cost and latency numbers, plus seven undocumented gotchas you only find after a month in production.
I started wiring Firebase Genkit into the back end of my indie apps in early 2026. The first thing I shipped was a small Flow that summarized user reviews into a dashboard. That alone replaced about eighty lines of Cloud Functions code I had been maintaining for two years — the usual "call Gemini, watch the token budget, format the result, log to Firestore" routine — with a single defineFlow call. I sat at my desk for a while just looking at the diff.
I am Masaki Hirokawa, an indie developer and contemporary artist based in Tokyo. I taught myself programming in 1997 when I was sixteen, and have been shipping iOS and Android apps as a solo developer since 2014. My wallpaper, mindfulness, and law-of-attraction apps have cumulatively passed 50 million downloads, and they are funded entirely by AdMob — which means I have to watch every second of latency and every dollar of cost. With that lens, I have been running Firebase Genkit and Gemini API in production for the last several weeks to see what really happens once you move past the tutorial code.
This article is the operational notebook that came out of that work. It still covers the fundamentals — Flow design, Tool composition, RAG, deployment, and security — but it also adds the things the official docs do not warn you about: where it hurts in production, what the real cost and latency numbers look like, and how I arrange Genkit around the constraints of an AdMob-funded indie business.
Understanding Firebase Genkit: Core Concepts and Advantages
Firebase Genkit consolidates the fragmented landscape of LLM integration patterns into a unified abstraction called the Flow. Rather than juggling PromptTemplate classes, Chain objects, and Agent frameworks from different libraries, Genkit provides a consistent mental model that scales from simple text generation to complex multi-step autonomous systems.
The Three Core Abstractions
Flows are the fundamental unit of AI computation. Each Flow has:
Type-safe inputs and outputs: Defined via Zod schemas, ensuring compile-time correctness and IDE autocomplete support
Built-in streaming: Native support for real-time response streaming, critical for modern UX where users expect immediate feedback
Automatic tracing: Every API call, intermediate result, and error is captured automatically, transforming production debugging from guesswork into forensic analysis
Composability: Flows can call other Flows, enabling modular architecture and code reuse
Tools are discrete actions that Flows invoke—API calls, database queries, file system operations. Gemini's built-in tool calling capability means the model itself decides which Tools to use and in what sequence, creating truly autonomous behavior.
Prompts are templated instructions with variable injection. They're more than simple strings; they're structured definitions that can include system instructions, few-shot examples, and contextual information formatted for optimal model understanding.
Embedders and Retrievers form the foundation of RAG systems. Embedders convert text to dense vectors, while Retrievers perform similarity searches across your knowledge base, augmenting the model's input with relevant context.
Initial Setup: Integrating with Gemini 2.5 Pro and Flash
Getting Genkit running with Gemini requires minimal boilerplate:
// genkit.tsimport { genkit } from 'firebase-genkit';import { googleAI, gemini15Pro, gemini15Flash } from '@genkit-ai/gemini';const ai = genkit({ plugins: [ googleAI({ apiKey: process.env.GOOGLE_API_KEY }) ], model: gemini15Pro // Default model for all operations});export default ai;
Configuration best practices:
Always externalize API keys via environment variables. In production, use Google Cloud Secret Manager. For local development, use a .env.local file. Never hardcode credentials or use real key formats like AIzaSy.... Instead, use placeholder values like YOUR_GEMINI_API_KEY when documenting examples.
// Multi-environment configurationconst getApiKey = () => { if (process.env.ENVIRONMENT === 'production') { return process.env.GEMINI_API_KEY; // From Secret Manager } return process.env.LOCAL_GEMINI_KEY; // From .env.local};if (!getApiKey()) { throw new Error('GEMINI_API_KEY not configured');}
The choice between Gemini 2.5 Pro and Flash depends on your specific requirements. Flash excels at speed and cost for throughput-oriented applications, while Pro delivers better reasoning for complex tasks.
✦
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
✦One month of real cost, latency, and p95 numbers from a Gemini 2.5 Flash review-summarization Flow running on a single indie iOS/Android app
✦Seven gotchas the official docs do not mention (35–45s cold starts, concurrency defaulting to 1, double-counted runFlow traces, Secret Manager billing, Flow rename breaking dashboards, PII in traces, streamCallback back-pressure)
✦Cloud Functions vs. Cloud Run decision matrix for AdMob-monetized apps, plus the temperature, cache-TTL, and model-selection rules I follow in production
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.
import { defineFlow } from 'firebase-genkit';import { gemini15Pro } from '@genkit-ai/gemini';import { z } from 'zod';export const summarizeArticle = defineFlow( { name: 'summarizeArticle', inputSchema: z.object({ text: z.string().describe('Article text to summarize'), maxLength: z.number().default(200).describe('Maximum summary length') }), outputSchema: z.string() }, async (input) => { const response = await ai.generate({ model: gemini15Pro, prompt: `Summarize the following text in ${input.maxLength} characters or less:\n\n${input.text}`, config: { temperature: 0.3, // Lower temperature for factual summarization maxOutputTokens: 500 } }); return response.text(); });
Streaming-Enabled Flow:
The ability to stream responses is critical for user-facing applications. Instead of waiting for the entire response, you can send chunks to the client as they arrive.
Real-World Implementation: AI-Powered Customer Support
A complete system that handles support tickets with autonomous AI assistance:
// Fetch support ticket detailsconst fetchSupportTicket = defineTool( { name: 'fetchSupportTicket', description: 'Retrieve support ticket by ID', inputSchema: z.object({ ticketId: z.string() }), outputSchema: z.object({ id: z.string(), customerId: z.string(), issue: z.string(), status: z.string(), priority: z.enum(['low', 'medium', 'high']) }) }, async (input) => { const response = await fetch( `https://support-api.example.com/tickets/${input.ticketId}` ); return response.json(); });// Update ticket with AI responseconst respondToTicket = defineTool( { name: 'respondToTicket', description: 'Send response to support ticket', inputSchema: z.object({ ticketId: z.string(), response: z.string(), status: z.enum(['open', 'waiting', 'resolved']) }), outputSchema: z.object({ success: z.boolean() }) }, async (input) => { const response = await fetch( `https://support-api.example.com/tickets/${input.ticketId}/respond`, { method: 'POST', body: JSON.stringify({ message: input.response, status: input.status }) } ); return { success: response.ok }; });// Main support agentexport const supportAgent = defineFlow( { name: 'supportAgent', inputSchema: z.object({ ticketId: z.string() }) }, async (input) => { // Retrieve ticket const ticket = await runFlow( defineFlow({ name: 'fetch' }, async () => await fetchSupportTicket.invoke({ ticketId: input.ticketId }) ), {} ); // Generate response const response = await ai.generate({ model: gemini15Pro, tools: [respondToTicket], prompt: `Support Ticket #${ticket.id}Priority: ${ticket.priority}Issue: ${ticket.issue}Provide a helpful response. If the issue can be resolved, mark as resolved.If more information is needed, mark as waiting. `, maxIterations: 2 }); return response.text(); });
Seven Gotchas the Official Docs Do Not Mention
The following are issues I actually hit while running Firebase Genkit on my own back end for a month. They are listed roughly in the order you are likely to step on them, not by severity.
1. Cold Start with the First Gemini Call Lands at 35–45 Seconds
On Cloud Functions Gen2 with Genkit and Gemini 2.5 Pro, my measured cold-start-to-first-token latency was 35–45 seconds. The breakdown was roughly 8–12 s for the container, 4–6 s for Genkit initialization (the googleAI plugin resolution inside genkit({...})), and 20–25 s for the first generate call against Pro.
Switching the model to Flash brought the response portion down to 8–12 s, but the cold start envelope still sat at 20–28 s. Because my apps live on AdMob revenue and any user-facing delay risks impressions, I stopped putting Gemini on user-tap code paths and split the workload into background batch summarization plus a Cloud Run container with minInstances: 1 for anything synchronous.
# firebase.json — bigger memory + a single warm instance to absorb cold starts{ "functions": { "runtime": "nodejs18", "memory": "1GiB", "concurrency": 80, "minInstances": 1 }}
minInstances: 1 adds a few dollars a month in my setup, but the long tail of cold-start latency essentially disappears. For an AdMob-monetized app where higher eCPM windows are the wrong place to make users wait, this paid for itself.
2. The Concurrency Default Stays at 1 Even on Gen2
The Cloud Functions Gen2 docs list concurrency limits up to 1000, but on multiple deploys with default settings I caught my containers running with concurrency 1. Because Gemini calls spend almost all their wall time waiting on I/O, concurrency 1 means a burst of 10 simultaneous requests spins up 10 containers — and you eat the cold-start tail and the startup cost for each one.
I now set concurrency: 50 as a floor for any Gemini-calling function, and 80 for Flash-heavy paths. Functions that use Pro stay around 30 because they push memory harder.
3. runFlow Nesting Double-Counts Tokens in Traces
Genkit's tracing is excellent, but if you call runFlow(childFlow, ...) inside runFlow(parentFlow, ...), the Cloud Logging trace export attributes token usage to both flows. At one month-end I found my Genkit trace summary was about 1.8× the actual Cloud Billing line item for the Generative Language API, which was confusing for ten minutes.
The fix is operational, not technical: treat Cloud Billing's generativelanguage.googleapis.com line as the source of truth for cost, and keep Genkit traces strictly for debugging — do not surface them on a dashboard alongside billing numbers.
4. Secret Manager accessSecretVersion Adds Up If You Call It Per Cold Start
accessSecretVersion is cheap at roughly $0.03 per 10,000 calls, but if every cold start hits it, the cost is small but the latency adds another 150–300 ms on top of an already painful cold start. I now grab the API key once at module load and cache it in module scope. Warm invocations skip the call entirely.
let cachedApiKey: string | null = null;async function getCachedSecret(name: string): Promise<string> { if (cachedApiKey) return cachedApiKey; const [version] = await secretManager.accessSecretVersion({ name }); cachedApiKey = version.payload.data.toString("utf8"); return cachedApiKey;}
5. Flow Names Are Plain Strings — Renaming Silently Breaks Your Dashboards
defineFlow({ name: 'summarizeArticle', ... }) writes that exact string straight into Cloud Logging labels. I once renamed a Flow to summarizeArticleV2 during a refactor, and it took me three days to notice that my monitoring dashboards and Slack alerts were still filtering on the V1 name and showing nothing.
When you rename, rewrite your queries to a wildcard like name=~"summarizeArticle.*" first, then migrate. The order matters.
6. Traces Capture PII — Add a Redactor Before Shipping
Genkit's auto-tracing writes Flow inputs straight into Cloud Logging. The review-summarization Flow I was running occasionally received user text containing email addresses or phone numbers, and I only noticed after the fact when scrolling through logs.
Wrap the Flow entry point with a redactor for anything that looks like PII. My version is intentionally crude — email, phone, credit-card-sized digit sequences — and runs before the input ever reaches defineFlow.
For any app that funnels user-generated text into Gemini, skipping this means PII sits in Cloud Logging for the default 30-day retention window.
7. streamCallback Is Synchronous — Heavy Work Inside It Causes Back-Pressure
The streamCallback you pass to a streaming Flow is invoked synchronously every time Gemini delivers a chunk. If you naively put a database write or an outbound API call in there, the time that takes blocks the next chunk from being consumed, and your end-to-end latency roughly doubles or triples.
Keep streamCallback to a queue.push(chunk)-style operation and offload the actual work to a separate worker or setImmediate boundary.
const bufferQueue: string[] = [];let flushTimer: NodeJS.Timeout | null = null;const streamCallback = (chunk: string) => { bufferQueue.push(chunk); if (!flushTimer) { flushTimer = setImmediate(() => { const batch = bufferQueue.splice(0); // Heavy work (DB writes etc.) happens here, off the streaming path persistChunks(batch); flushTimer = null; }); }};
What It Actually Costs and How Fast It Actually Is
These numbers come from a single Flow I ran in production for a month: a review summarizer that processes roughly 12,000 reviews per month for one of my wallpaper apps.
Per-Model Cost and Latency (April 2026 measurements)
Model
Avg Input / Output Tokens
Avg Latency
p95 Latency
Cost per Request
Monthly Cost (12,000 req)
Gemini 2.5 Pro
1,800 / 250
4.2 s
11.8 s
$0.018
~$216
Gemini 2.5 Flash
1,800 / 250
1.1 s
3.4 s
$0.0014
~$17
Gemini 2.0 Flash
1,800 / 250
0.9 s
2.7 s
$0.0009
~$11
For a task like "clean up and summarize Japanese review text," 2.5 Flash gave me output quality I was happy with. Cost dropped roughly 12.7× and latency 4× compared with Pro. I eventually settled on Flash and run the whole pipeline for about $17 a month.
End-to-End Latency Including Cold Start (Cloud Functions Gen2)
State
Pro
Flash
Warm (immediately after a call)
4.2 s
1.1 s
Cool (after 5+ minutes idle)
28 s
20 s
Cold (first invocation)
42 s
24 s
Cold + minInstances=1
5.0 s
1.4 s
Adding minInstances: 1 cost me about $4 a month for this configuration. I do not pay it for the asynchronous review summarizer, but I do pay it for the FAQ bot users hit directly.
Cache Hit Rate Compression
// Effective cost after Firestore cache for identical review bodiesconst CACHE_HIT_RATE = 0.18; // ~18% in my dataconst ORIGINAL_COST_PER_MONTH = 17; // Flash baselineconst EFFECTIVE_COST = ORIGINAL_COST_PER_MONTH * (1 - CACHE_HIT_RATE);// → ~$14/month (another $3 off)
Review summarization has a low cache hit rate because few reviews are identical. A FAQ bot, on the other hand, sees cache hit rates of 50–65% in my measurements, dropping a $17/month workload to roughly $7.
Designing Around Genkit in AdMob-Funded Indie Apps
Closing with what I have actually deployed: the design rules I follow for indie apps where a one-second latency change can move the AdMob eCPM enough to matter.
Pattern A: Keep Gemini Out of the User-Tap Path
In my wallpaper apps, the home screen, search, and download flows do not call Gemini at all. Those screens have the highest AdMob impression density, and slowing them down hurts ad revenue directly.
The places I do use Gemini are explicit "Generate" buttons — name this wallpaper, write a description for this collection — where users have asked the app to think for a moment. Loading states are expected there, so even Pro is fine.
Pattern B: Run Genkit Flows During Off-Peak Batch Windows
Tag generation for new wallpapers, review summarization, SEO description rewriting — anything that does not need to block a user — runs in Cloud Scheduler batches in my local off-peak window (02:00–05:00 JST).
Cold starts in this window do not matter, and the Gemini rate limits are far easier to live with. I push roughly 80,000 auto-tag operations per month through this path on Flash, for about $25/month.
Pattern C: FAQ / Support Bots — Flash + Aggressive Caching, Temperature Locked Down
For support FAQ search, where users will accept a few seconds, I stay on 2.5 Flash and put a 24-hour Firestore cache in front. Identical questions return from cache; new questions hit Flash.
I keep temperature: 0.2 because creative FAQ answers actually increase support load. When I ran a period at temperature: 0.7, several users wrote in based on Gemini's invented usage instructions and we ended up doing manual cleanup — so this is one knob I deliberately pin low.
Cloud Functions vs. Cloud Run — A Working Decision Matrix
Workload
Recommendation
Why
User-tap, must respond fast
Cloud Run + minInstances=1 + Flash
Cold start is unacceptable
Off-peak batch, high volume
Cloud Functions Gen2 + tuned concurrency
Bursty startup, low fixed cost
Streaming required
Cloud Run (Functions SSE tops out at 60 s)
Functions SSE will time out
Long-running, infrequent
Cloud Functions, minimum config
Per-invocation cost beats always-on
Long-running, high volume
Cloud Run + high concurrency
One container multiplexes many requests
My current production layout is Cloud Functions Gen2 for review summarization and tag generation, plus a single Cloud Run service for the FAQ bot. For an AdMob-funded business where daily revenue swings, keeping the fixed minInstances footprint tight has been worth the discipline.
Wrapping up and Next Steps
After a month of running Firebase Genkit in production, the honest summary is that operations got quieter. The token-budget checks, retry loops, and structured logging that used to be scattered across my Cloud Functions now collapse into Flow, Tool, and tracing — and code reviews have one fewer thing to argue about.
But as this article has tried to show, you only get there if you walk around the seven undocumented potholes: cold-start tails, the silent concurrency=1 default, double-counted runFlow traces, Secret Manager call patterns, dashboards that depend on Flow names, PII in trace logs, and the synchronous streamCallback. Miss any of them and your costs or latency drift to roughly 2–3× of what you planned.
If you are adding Genkit now, I would start with a non-user-facing batch use case on Flash, measure cost and latency for a few weeks, and only then promote a piece of it to a warm Cloud Run service. That is the path I followed, and it kept the AdMob math intact while I learned what the framework actually does in production.
The next practical step is to look at your own work or app and find one task that a human repeats every day. Replace that single task with one defineFlow. Genkit's real value shows up in how cleanly that small thing can grow afterwards.
If you are wiring Gemini into your own indie back end, I hope these notes save you a few of the days I spent on the same questions. Thank you for reading.
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.