●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
Coalescing Gemini API Requests with SSE Fan-out: Collapsing 100 Simultaneous Hits into a Single Call
How I rebuilt the post-push-notification thundering herd on a 50M-download wallpaper app into a Cloudflare Durable Objects coalescer with SSE fan-out, cutting Gemini API costs by 92% with 14 days of production telemetry.
I've been shipping indie apps since 2014, and the moment that still scares me most is the first ten seconds after a push notification goes out. A server that normally hums along at 5 requests per second gets hit with 800–1,200 simultaneous calls. Gemini API costs scale linearly, p99 latency easily blows past 9 seconds, and Crashlytics fills up with timeout reports.
After being burned by this "10-second cliff" too many times on a wallpaper app that now sits at over 50 million cumulative downloads, I rebuilt the post-notification path around request coalescing and SSE fan-out on Cloudflare Durable Objects. The new shape cut Gemini calls during the 10-second cliff to 1/12 and brought monthly API costs down by 92%. The code below is the version that ran in production for 14 consecutive days, cross-checked against Crashlytics and AdMob reports.
Why "just cache it" doesn't reach the problem
My first attempt was a KV-backed response cache keyed on a SHA-256 of the prompt, with a 5-minute TTL. That worked beautifully after minute 6. Inside the cliff, however, the problem isn't cache hits — it's the cache-miss thundering herd.
When a notification lands, 1,200 devices wake up at roughly the same instant and send the same prompt (today's recommended wallpaper, this week's themed bundle, etc.). The first request starts calling Gemini; the remaining 1,199 also see a cache miss and start calling Gemini in parallel. The cache isn't written until 1.5 seconds later, by which point 1,199 redundant requests have already been billed.
Coalescing patches exactly this gap. Inflight requests sharing the same prompt hash get folded into one Gemini call, and when the response arrives it fans out to every waiting requester. The idea is elementary, but in a production setting with Stripe webhooks and AdMob anomaly detection sharing the runtime, the design has five non-obvious decisions baked into it.
Decision 1: how to choose the coalescing key
My first prototype keyed on the SHA-256 of the full prompt. That maximizes coalescing in theory, but the moment per-user variables (locale, device language, last wallpaper viewed) sneak into the prompt, the miss rate explodes and most requests stop coalescing at all.
The pattern I ended up with is "template ID + shared variables" as the key, with per-user variables injected at a later layer. The prompt gets restructured like this:
type PromptSpec = { templateId: string; // e.g. "daily-recommend-v3" sharedVars: Record<string, string>; // e.g. { dayOfYear: "147", theme: "calm" } perUserVars: Record<string, string>; // e.g. { lastViewed: "wp-12345", locale: "ja-JP" }};async function coalesceKey(spec: PromptSpec): Promise<string> { const payload = JSON.stringify({ t: spec.templateId, s: spec.sharedVars, }); const buf = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(payload)); return [...new Uint8Array(buf)].map(b => b.toString(16).padStart(2, "0")).join("");}
Per-user variables get appended to the prompt as a "reader profile" section, and Gemini is asked via structured output to return N candidates. Filtering and reordering happen per-user inside the Worker. That separation lifted the coalescing hit rate from 18% to 71% and actually shaved 80ms off p99 — the Worker-side filtering runs in parallel and is much cheaper than a redundant Gemini call.
✦
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
✦Durable Object code that coalesces 100 simultaneous identical-prompt hits into one Gemini call (measured 12x cost reduction)
✦The coalescing key design used in the 50M-download wallpaper app, and why a 5-second TTL window beat a fixed 1-second one (80ms p99 swing)
✦Promotion strategy when the primary stream drops mid-flight, plus the SSE backfill implementation for reconnecting clients
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.
Coalescing has a time window. A long window catches more requests but adds latency to the very first one. A short window keeps latency tight but reduces the herd-collapsing effect.
Fourteen days of measurement against AdMob reports on the wallpaper app pointed me at a hybrid: "TTL = 5 seconds during the 5-second window immediately after a push notification, TTL = 200ms otherwise."
Seconds after notification
TTL window
Coalesce rate
Felt p99
0–5s
5s
91%
980ms
6–60s
200ms
24%
720ms
61s+
200ms
8%
690ms
The "notification active" flag lives in the Worker Cache API with a 6-second TTL, written by the notification dispatcher. The Durable Object reads that flag to pick a TTL. Compared to the previous fixed TTL=1s, costs dropped another 38% and p99 didn't degrade.
Decision 3: Durable Object granularity
Where you draw the Durable Object instance boundary directly determines coalescing performance. "One global instance" is obviously a single point of failure. "One instance per coalesceKey" sounds clean but pays cold-start cost on every key.
I settled on "one Durable Object per template ID." For an indie operation, there are typically 30–40 templates, of which only about 10 stay warm given Cloudflare's geographical distribution. Each DO holds an in-memory Map<coalesceKey, Promise<Response>> and resolves coalescing entirely within itself.
Storing the Promise itself in inflight is the key — late arrivals just await it. The catch branch must always call inflight.delete; without it, an error leaves a zombie Promise occupying the slot forever. I learned that the hard way one evening when Crashlytics lit up with 200 "Gemini timeout 30s" reports in a row.
Decision 4: SSE fan-out and the "primary departure" problem
Fan-out is easy when the response is a single JSON blob. With Gemini streaming, it gets noticeably harder. While one request is receiving streamed bytes from Gemini, the other 99 need to receive the same bytes concurrently.
The nastiest failure mode here is "primary departure." If the request that opened the connection to Gemini drops first — the user closes the screen, the network flips — the fan-out to the other 99 dies with it.
My fix was to give the Durable Object a background task that owns the Gemini connection independently of any single client.
async streamCoalesce(key: string, prompt: PromptSpec): Promise<ReadableStream> { let producer = this.streams.get(key); if (!producer) { producer = this.startGeminiStream(prompt); this.streams.set(key, producer); } return producer.subscribe();}private startGeminiStream(prompt: PromptSpec): StreamProducer { const buffer: string[] = []; const subscribers = new Set<WritableStreamDefaultWriter>(); let complete = false; // Gemini connection is owned by the DO, not by any subscriber (async () => { const stream = await callGeminiStreaming(prompt); for await (const chunk of stream) { buffer.push(chunk); for (const sub of subscribers) { try { await sub.write(chunk); } catch { subscribers.delete(sub); } } } complete = true; })(); return { subscribe(): ReadableStream { const { readable, writable } = new TransformStream(); const writer = writable.getWriter(); // backfill prior chunks for (const c of buffer) writer.write(c); if (complete) writer.close(); else subscribers.add(writer); return readable; } };}
The buffer collects all chunks seen so far so that a late subscriber gets backfilled before catching up to the live stream. The very concept of "primary departure" disappears.
The trap is Durable Object memory and CPU limits. Per-stream buffers above ~50 KB get problematic, so I added a scheduler via state.storage.setAlarm that drops the buffer 30 seconds after the last subscriber closes.
Decision 5: backfill for reconnecting clients
In a notification-driven environment, reconnects are routine. AdMob ad-load blocks the main thread, iOS URLSession breaks on background resume — the causes are varied. Crashlytics shows reconnects account for 17% of all stream lifetimes.
Reconnecting clients send a Last-Event-ID header indicating how far they got, and the Durable Object resumes from its buffer. That's vanilla SSE behavior, but combining it with the fan-out design has a nice side effect: most reconnecting clients can still board the same in-flight stream rather than triggering a fresh Gemini call.
Over 14 days of telemetry, 84% of reconnects landed on an in-flight stream, with only 16% falling back to the KV snapshot. Even that fallback doesn't call Gemini again.
Measured: 14 days of cost and latency
The table below covers 14 evening (21:00 JST) push notifications during late April 2026. The baseline is the same traffic pattern under the previous KV-only setup running through March 2026.
Metric
KV cache only
Coalescing + SSE fan-out
Change
Gemini calls in the 10s post-notification window (per push)
avg 712
avg 58
1/12
p99 latency within the 10s window
9.2s
1.0s
-89%
Monthly Gemini API spend
¥48,200
¥3,820
-92%
"Gemini timeout 30s" Crashlytics reports / month
1,140
13
-99%
Past the 30-second mark, the coalesce rate drops sharply, but latency requirements relax too, so I let the system fall back to plain KV caching.
My recommended decision order for indie developers
I've applied this design to the wallpaper app and several other indie projects. For a personal scale, deciding whether to adopt this architecture comes down to a quick four-step gate.
Do you see 50 or more identical-prompt requests within 10 seconds of a push notification or email blast? Below 50, a plain KV cache is fine and the Durable Object maintenance cost isn't worth it.
Can your prompts be split into a template plus per-user variables? Without that split, the coalescing key collides too often and the win evaporates.
Are you calling Gemini in streaming mode? Single-shot JSON makes coalescing trivial; streaming requires subscribe/backfill scaffolding.
Does your monthly Gemini API spend exceed ¥10,000? Below that, the engineering effort rarely pays back.
If any answer is "no," start with a simple KV cache and in-process dedup (a plain Map). Since teaching myself programming in 1997, I've repeatedly thrown out simple solutions in favor of clever architectures and regretted it. Less code usually wins.
A back-of-the-envelope cost estimator
Here's the function I use at the final go/no-go gate. It's deliberately rough, assumes Cloudflare Workers + Durable Objects pricing, and produces savings estimates accurate enough to decide.
Actual production savings exceed the estimate slightly. The reasons are AdMob-triggered re-entries and reconnecting clients that opportunistically board later coalesced streams. As a rule of thumb, if the estimator predicts a 70%+ reduction, the architecture is worth shipping.
Production gotchas
Three things that bit me during the first week:
1. Durable Object hibernation wipes the in-memory Maps. After 30 seconds of inactivity the DO hibernates and both inflight and cache Maps vanish. This is by design, but the moment the DO re-warms, coalescing collapses and a fresh burst hits Gemini. The fix is a state.storage.setAlarm set on a 25-second cadence to keep the DO active. Monthly DO request billing goes up a little, but the saved Gemini spend dwarfs the difference.
2. Structured-output schema violations fan out to every caller. If Gemini returns a payload that fails JSON-schema validation, all 100 coalesced clients receive the same error. The mitigation: validate inside the DO with a Pydantic-style guard, and on failure demote the request to individual retry — abandoning coalescing rather than amplifying the bug.
3. AdMob ad-load contention. When a fresh app launch fires AdMob and the Gemini recommendation request in parallel, iOS URLSession ends up arbitrating bandwidth. Delaying the Gemini subscribe by 200ms until AdMob finishes loading dropped Crashlytics ANR counts by 38%.
That's the full picture of the coalescing + fan-out design I've been running alongside the 50M-download wallpaper app as an indie developer. The architecture looks ornate on a whiteboard, but in operation it feels like quiet, dependable infrastructure — the kind of optimization that comes from staring at one specific cost curve for too long. If you're feeling the same 10-second cliff after every push, hopefully this maps cleanly onto your setup.
Looking back at running the business since 2014, the most useful insight from 50M cumulative downloads is that "cost optimization is like sculpting — the more you strip away, the clearer the shape becomes." Next on my list is wiring a model router between Gemini Flash and Pro per coalescing key, which I'll write up separately.
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.