●MODEL — Gemini 3.5 Flash is GA, Google's most intelligent model for sustained frontier performance on agentic and coding tasks●AGENTS — Managed Agents in the Gemini API enter public preview, running autonomous agents in Google-hosted isolated Linux sandboxes●IMAGE — Nano Banana 2 (Gemini 3.1 Flash Image) and Nano Banana Pro (Gemini 3 Pro Image) are now GA●IMAGE — Video-to-image generation arrives: pass a video as context to create thumbnails, posters, and infographics (3.1 Flash Image only)●DEPRECATION — gemini-3.1-flash-image-preview and gemini-3-pro-image-preview shut down on June 25; migrate to GA●STUDIO — Gemini 3 is available across the Gemini app, AI Studio, and Vertex AI●MODEL — Gemini 3.5 Flash is GA, Google's most intelligent model for sustained frontier performance on agentic and coding tasks●AGENTS — Managed Agents in the Gemini API enter public preview, running autonomous agents in Google-hosted isolated Linux sandboxes●IMAGE — Nano Banana 2 (Gemini 3.1 Flash Image) and Nano Banana Pro (Gemini 3 Pro Image) are now GA●IMAGE — Video-to-image generation arrives: pass a video as context to create thumbnails, posters, and infographics (3.1 Flash Image only)●DEPRECATION — gemini-3.1-flash-image-preview and gemini-3-pro-image-preview shut down on June 25; migrate to GA●STUDIO — Gemini 3 is available across the Gemini app, AI Studio, and Vertex AI
Before Free Users Quietly Eat Your Margin: Tier Design and Cost Ceilings for Gemini API Apps
Protecting the margin on a Gemini-powered app means designing around a per-user monthly cost ceiling, not request counts. Tier-aware model routing, real-cost metering in KV, and the token-bloat traps that drain profit, with working code.
As an indie developer, the first invoice for an AI feature trips you up in the same place every time: money leaves with every request, a structure traditional apps never had. If you carry over the instincts of an AdMob-and-IAP business and open the Gemini API without limits, the more users you gain, the deeper the loss can run.
The awkward part is that the lead actor in that loss is your free users. The cost of paying users is recovered through revenue, but the API cost free users burn is paid by no one. Polish the feature all you like — without containing this in the design, growth itself widens the deficit.
This piece covers how to avoid stumbling at the request-limit doorway, and how to judge a per-user monthly cost ceiling at the edge, in the order these things actually mattered in production. The models assumed are the generally available Gemini 3.5 Flash as of June 2026, the lighter Gemini 3.1 Flash-Lite, and the higher-end Gemini 3.1 Pro.
Stop counting requests first
The "3 free uses per day" design that so many apps reach for is wrong twice over.
First, on revenue: users churn the instant they exhaust those three, uninstalling before they ever feel the value. Second, on cost: the tokens a single request burns swing easily by 100x, from a 200-token quick question to tens of thousands when a long document is passed in whole. As long as you count requests, the real cost stays invisible.
What landed for me in production was to split the two: charge by depth of experience, but manage cost by real consumption — tokens are money. The same "summarize" feature is convenient on free and work-grade on paid. Defense, meanwhile, draws its ceiling from the real cost accumulated per user. That two-layer stance is the foundation.
Pin models to tiers to fix the cost structure
Map the expensive Pro and the cheap Flash-Lite directly onto user tiers. That alone keeps free-user cost inside the Flash-Lite band while paid growth flows straight to revenue. Your investment in free users (their API cost) runs as a capped, forward outlay toward future conversion.
Tier
Model
Output cap
Thinking budget
Role
free
gemini-3.1-flash-lite
1,024
0
Feel the value; entry to conversion
pro
gemini-3.5-flash
8,192
low
Everyday usable quality
premium
gemini-3.1-pro
8,192
high
Deep analysis; top quality
import osfrom google import genaifrom google.genai import typesclient = genai.Client(api_key=os.environ["GEMINI_API_KEY"])# Single source of truth: tier -> (model, output cap, thinking budget).# Keeping it in one place lets a price change or model bump be a one-line follow.TIER_PROFILE = { "free": ("gemini-3.1-flash-lite", 1024, 0), "pro": ("gemini-3.5-flash", 8192, 2048), "premium": ("gemini-3.1-pro", 8192, 8192),}async def analyze(content: str, user_tier: str) -> dict: model, max_out, think = TIER_PROFILE.get(user_tier, TIER_PROFILE["free"]) # Free gets a short system prompt. A long prompt rides on every request as a # fixed cost, so we deliberately trim it on free (see "Leak 1" below). system = ( "Give exactly three concise key points." if user_tier == "free" else "Structure root causes, risks, and prioritized, concrete improvements." ) config = types.GenerateContentConfig( system_instruction=system, max_output_tokens=max_out, temperature=0.7, thinking_config=types.ThinkingConfig(thinking_budget=think), ) resp = await client.aio.models.generate_content( model=model, contents=content, config=config, ) # usage_metadata is the lifeline of real-cost metering. Always carry it back. u = resp.usage_metadata return { "text": resp.text, "model": model, "in_tokens": u.prompt_token_count, "out_tokens": u.candidates_token_count, }
The key is to always bring usage_metadata home. Holding the measured token counts, not an estimate, is what makes the cost ceiling that follows actually work.
✦
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
✦Mapping tiers to Flash-Lite / Flash / Pro so free-tier cost is structurally capped
✦Judging a per-user monthly cost ceiling at the Cloudflare Workers edge, before any request reaches the API
✦Closing the three quiet leaks — long system prompts, growing chat history, and unbounded retries
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.
Prices get revised. Write a fixed unit price into an article and it may be stale by the time you read it. So on the design side, escape the unit price into a single constant, multiply it by measured tokens, and accrue. Always confirm the latest rate on the official pricing page and swap only this constant.
# USD per million tokens. ALWAYS update to the official current value.# Differs by model and by input/output. This is the single point you swap.PRICE = { "gemini-3.1-flash-lite": {"in": 0.10, "out": 0.40}, "gemini-3.5-flash": {"in": 0.30, "out": 2.50}, "gemini-3.1-pro": {"in": 1.25, "out": 10.00},}def estimate_cost_usd(model: str, in_tok: int, out_tok: int) -> float: p = PRICE[model] return (in_tok * p["in"] + out_tok * p["out"]) / 1_000_000
Call estimate_cost_usd per request and accrue it per user per month. Because you accrue dollars, not counts, light questions stay far from the ceiling while heavy work approaches it fast — behavior that matches intuition. The classic accident, a few heavy users melting your margin, gets contained naturally.
Judge the monthly cost ceiling at the edge
Do the judging before the request reaches origin, at the Cloudflare Workers edge. Block abuse and overuse before they touch the Gemini API. If it never arrives, the cost is zero. The only difference from a rate limit is that what you accrue in KV is USD, not counts.
interface Budget { spentUsd: number; // accumulated cost this month resetAt: number; // next-month reset, Unix(ms)}interface Env { BUDGET_KV: KVNamespace; }// Per-tier monthly cost ceiling (USD). Back it out from price x expected use.const COST_CEILING: Record<string, number> = { free: 0.15, // for free you control the deficit directly pro: 3.00, // ceiling that still leaves margin within the subscription premium: 12.00,};async function withinBudget( env: Env, userId: string, tier: string): Promise<{ ok: boolean; spent: number; ceiling: number }> { const ceiling = COST_CEILING[tier] ?? COST_CEILING.free; const key = `budget:${userId}`; const now = Date.now(); const b = (await env.BUDGET_KV.get(key, "json")) as Budget | null; // Start from 0 if there's no current month or we're past the reset. const cur = b && b.resetAt > now ? b : { spentUsd: 0, resetAt: endOfMonth(now) }; return { ok: cur.spentUsd < ceiling, spent: cur.spentUsd, ceiling };}// Add real cost after the response (pass the value derived from usage_metadata).async function addSpend(env: Env, userId: string, costUsd: number) { const key = `budget:${userId}`; const now = Date.now(); const b = (await env.BUDGET_KV.get(key, "json")) as Budget | null; const cur = b && b.resetAt > now ? b : { spentUsd: 0, resetAt: endOfMonth(now) }; cur.spentUsd += costUsd; await env.BUDGET_KV.put(key, JSON.stringify(cur), { expirationTtl: Math.ceil((cur.resetAt - now) / 1000) + 86400, });}function endOfMonth(now: number): number { const d = new Date(now); return Date.UTC(d.getUTCFullYear(), d.getUTCMonth() + 1, 1);}
For a user who hits the ceiling, return an upgrade path, not an error. A 429 that says "you've used this month's free allowance — go Pro to continue" is a stronger conversion driver the lower the remaining balance. The defensive mechanism becomes the entry to revenue.
export default { async fetch(req: Request, env: Env): Promise<Response> { const { userId, tier } = await verifyJwt(req); // implementation omitted const { ok, spent, ceiling } = await withinBudget(env, userId, tier); if (!ok) { return new Response(JSON.stringify({ error: "budget_exceeded", message: "Monthly limit reached. Upgrade to continue.", spent, ceiling, }), { status: 429, headers: { "Content-Type": "application/json" } }); } // Call Gemini here, then accrue the returned real cost via addSpend. return new Response("...", { status: 200 }); },};
KV is eventually consistent, so under very high concurrency a ceiling can be exceeded slightly. I run a two-step guard — soft warning at ceiling x 0.95, hard cut at x 1.1 — and tolerate small overruns. The goal is preventing accidents, not penny-exact accounting.
Three quiet leaks that drain margin
Tiers and a cost ceiling hold the broad line. What remains are leaks that grow tokens where you don't look. Here they are in the order I actually plugged them.
Leak 1: a long system prompt on the free tier. The system prompt is a fixed cost that rides on every request's input. Use a careful 2,000-token instruction for all users and the pure fixed cost swells as requests grow. Trim free to under 300 tokens and create the quality gap with the paid tier's detailed prompt — push the party that pays the fixed cost toward the layer that has revenue.
Leak 2: sending the whole chat history every time. Resend the full history each turn and input tokens grow linearly as the conversation lengthens. At 50 turns a single request's input hitting tens of thousands of tokens is routine. Keep only the last N turns in detail and compress earlier ones into a summary — a sliding window keeps tokens within a fixed band.
def build_window(messages: list[dict], recent_turns: int = 8, summary: str = "") -> list[dict]: """Keep only the last recent_turns turns; replace earlier ones with a summary.""" start = len(messages) - recent_turns * 2 # one turn = two messages if start <= 0: return messages recent = messages[start:] if not summary: return recent head = {"role": "user", "parts": [{"text": f"[Summary so far] {summary}"}]} return [head] + recent
Leak 3: unbounded retries on error. Retries without exponential backoff turn a single user action into 20-30 billable requests on a transient 503. Always add backoff, a retry cap, and a strict allow-list of which error types are even retryable.
import asyncio, randomfrom google.api_core.exceptions import ResourceExhausted, ServiceUnavailableasync def call_with_backoff(coro_factory, max_retries: int = 3): last = None for attempt in range(max_retries): try: return await coro_factory() except (ResourceExhausted, ServiceUnavailable) as e: last = e if attempt == max_retries - 1: break await asyncio.sleep(2 ** attempt + random.uniform(0, 1)) # with jitter except Exception as e: raise RuntimeError(f"non-retryable error: {e}") from e # stop immediately raise RuntimeError(f"retry cap exceeded: {last}")
Cost sets the price floor
The price of an app with AI features is set not by competitors but by API cost. Take the per-user monthly API cost, add store fees and infrastructure, and leave margin on top. As a rule of thumb, placing the floor at 10-15x the expected cost survives the swings in conversion. At $3/user/month of cost, the monthly floor is roughly $30-45.
That formula holds only because the tier design comes first — containing free with Flash-Lite and covering the paid layer's cost from revenue with Pro. Put everyone on Pro and the margin vanishes at the same price. Price is only the result that sits on top of the cost structure.
Put cost first in the design
Most failed AI monetizations start by deferring cost. Build the feature first, figure out billing later — that order fits poorly with a structure where money leaves on every request.
Reverse it. Decide the tier-to-model mapping first, accrue real cost via usage_metadata, draw the monthly ceiling at the edge. Then polish the free experience to feed conversion. Build the defense, then add the offense. That order alone turns growth toward profit rather than loss. As a first step, compute, just once and with measured rates, what your app costs per month if a thousand people use it daily. Start the design from there and the pain of rebuilding later shrinks a great deal.
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.