GEMINI LABJP
NANOLITE — Nano Banana 2 Lite is here: Google's fastest and most cost-efficient Gemini Image model, made for running lightweight image generation cheaplyOMNIFLASH — Gemini Omni Flash is in public preview, a natively multimodal model that lets enterprises and developers build custom, dynamic video workflowsAGENTS — Managed Agents expand with background: true for async server-side runs and polling, remote MCP server integration, and refreshing credentials across interactionsMEMORY — The Memory Bank IngestEvents API is generally available, decoupling event ingestion from memory generation so you can stream content continuouslyTHROUGHPUT — Provisioned Throughput now lets you submit up to seven pending orders for the same model and regionDEPRECATE — Image generation models shut down on August 17, and the Grok 4.1 family on the Gemini Enterprise Agent Platform on August 20NANOLITE — Nano Banana 2 Lite is here: Google's fastest and most cost-efficient Gemini Image model, made for running lightweight image generation cheaplyOMNIFLASH — Gemini Omni Flash is in public preview, a natively multimodal model that lets enterprises and developers build custom, dynamic video workflowsAGENTS — Managed Agents expand with background: true for async server-side runs and polling, remote MCP server integration, and refreshing credentials across interactionsMEMORY — The Memory Bank IngestEvents API is generally available, decoupling event ingestion from memory generation so you can stream content continuouslyTHROUGHPUT — Provisioned Throughput now lets you submit up to seven pending orders for the same model and regionDEPRECATE — Image generation models shut down on August 17, and the Grok 4.1 family on the Gemini Enterprise Agent Platform on August 20
Articles/API / SDK
API / SDK/2026-05-23Advanced

Idempotency Key Design for the Gemini API: Patterns I Use to Prevent Duplicate Generation Across Six Sites

After five months of running six AI-driven sites in parallel, I built an idempotency layer in front of the Gemini API to neutralize retry storms. This deep dive shares the SHA-256 + Cloudflare Workers KV design, the operational numbers behind it, and the four gotchas that only surface in production.

gemini-api277idempotency5cloudflare-workers7production140reliability7kv-store

Premium Article

I have been shipping iPhone and Android apps as a solo indie developer since 2014, and after my titles crossed a cumulative 50 million downloads, I started running into duplicate-event bugs everywhere — duplicate purchases, duplicate analytics events, duplicate push notifications. Lately I have been running six sites in parallel — Claude Lab, Gemini Lab, Antigravity Lab, Rork Lab, plus two non-tech blogs called Lacrima and Mystery — and as Gemini API call volume climbed, the same class of bug showed up again. This time it was the article auto-generation pipeline producing the same topic twice and very nearly pushing two MDX files for the same day.

Retries are a good thing. They are the first line of defense against transient network failures, server-side throttling, and timeouts, and you cannot run production without them. But retries are also a duplication factory. Gemini does not offer an Idempotency-Key header, which means clients have to enforce request uniqueness on their own. In this post I share the idempotency layer I have been running for five months across six sites, with the actual code, the actual numbers, and the four gotchas I only discovered in production.

Why the Gemini API Has No Built-in Idempotency

The Gemini API is a stateless, REST-style surface. A generateContent call is conceptually side-effect-free, which is why the server does not deduplicate "the same request arriving twice" for you. That stance makes sense from Google's side: LLM outputs are inherently probabilistic, so a second call returning a different completion is normal behavior, not a bug.

It is a different story from the operator's side. For a pipeline like mine — "generate exactly one article for this topic on this date" — duplicate generation is not acceptable. My first incident happened when a response arrived right before a Cloudflare Workers timeout. Workers requeued the job, the retry succeeded, and I had two complete articles for the same topic queued for push. The pre-push integrity check caught it, but if it had not, Google Search Console would have likely flagged duplicate content within days.

With no server-side mechanism to lean on, the responsibility lands on the client. There are three implementation styles to choose from: hash the request body and remember "recent" requests, generate a client-side UUID and persist it in a serverless KV, or maintain a separate queue of completed jobs. I ended up combining the first two in a hybrid design.

The Core Design — SHA-256 Hash Plus a Rolling Retry Window

The first decision is "what counts as the same request." Comparing raw byte sequences is too strict — a single timestamp or random seed in the payload will produce different keys for what is logically the same call. Comparing just the topic string is too loose, because there are situations where I deliberately want two variants of the same topic.

What I settled on is hashing only the semantically meaningful slice of the payload and combining that with a "retry window" — a short bucket of time. The TypeScript implementation looks like this:

// idempotency-key.ts
import { createHash } from "node:crypto";
 
export interface IdempotencyInput {
  // Semantically meaningful request body
  model: string;
  systemPrompt: string;
  userPrompt: string;
  responseSchema?: object;
  // Operational scope
  site: string;          // "gemini-lab" | "claude-lab" | ...
  pipeline: string;      // "daily-content" | "premium-thu" | ...
  // Retry window (rounded to 5-minute buckets)
  windowMinutes?: number;
}
 
export function buildIdempotencyKey(input: IdempotencyInput): string {
  const windowMinutes = input.windowMinutes ?? 5;
  const nowMs = Date.now();
  const windowStart = Math.floor(nowMs / (windowMinutes * 60_000)) * (windowMinutes * 60_000);
 
  // Canonical form: stable key order, trimmed whitespace
  const canonical = JSON.stringify({
    m: input.model,
    sp: input.systemPrompt.trim(),
    up: input.userPrompt.trim(),
    rs: input.responseSchema ?? null,
    site: input.site,
    pipeline: input.pipeline,
    w: windowStart,
  });
 
  const hash = createHash("sha256").update(canonical).digest("hex");
  return `idem:${input.site}:${input.pipeline}:${hash.slice(0, 32)}`;
}

I truncate the hash to 32 hex characters (128 bits) to fit comfortably within the KV key size limit (512 bytes) while keeping the prefix human-readable. With six sites and a few hundred requests per day, the birthday-paradox collision probability at 128 bits is effectively zero — you would need roughly $2^{64}$ keys before the first collision is statistically expected.

The five-minute retry window is the more interesting parameter. It declares "two requests with the same hash within five minutes are the same logical operation," which is comfortably longer than Gemini's typical timeout-and-retry cycle (tens of seconds) but short enough that an intentional regeneration tomorrow will produce a different key. I originally set it to one minute, then bumped it to five after observing that Cloudflare Workers' retry queue can re-inject jobs two to three minutes later under load.

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
Concrete TypeScript implementation of an idempotency key built from a SHA-256 hash plus a rolling retry window, tuned for serverless production
Cloudflare Workers KV TTL design and a deny-by-default fallback path for when the KV store itself becomes unreachable
Five months of real production data from six sites in parallel — request volume, duplicate detection rate, false positives, and the rationale behind each threshold
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

API / SDK2026-06-25
When Gemini's Structured Output Quietly Drifts From Your Schema — Field Notes on Measuring Validation and Retries
Even with response_schema set, Gemini's structured output occasionally drifts in production. Stop swallowing failures, measure them, split causes by finish_reason, and feed errors back for a corrected retry. Field notes from stabilizing a validation pipeline.
API / SDK2026-06-13
The Morning Gemini Generated Fine but the Publish Crashed — A 'Generation Outbox' So Expensive Output Is Never Lost
Generation succeeds, then the process dies right before publishing. The expensive output is gone, and you pay for the same generation again. Here is a 'generation outbox' that persists the output first and turns publishing into an idempotent follow-up, plus what it did for me during the June outage.
API / SDK2026-05-28
Running an SLO and Error Budget for the Gemini API as an Indie Developer — Guarding Four Sites with Burn-Rate Monitoring
Notes from running the Gemini API inside four production sites as an indie developer. A practical SLO and Error Budget design that fits a single-person operation: Cloudflare Workers and KV for burn-rate calculation, simplified multi-window alerts, and decision rules for what to freeze when the budget runs out.
📚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 →