◉GEMINI LABJP
●ARGON — gemini-4-argon is generally available as of 10/13, aimed at complex coding, agents, math and science●DEEP RES — The old Deep Research agent shut down on 10/23. If calls fail, move to deep-research-preview-04-2026●NANO — gemini-nano-banana-2.1 is GA (10/06). gemini-3.1-flash-image is deprecated with no shutdown date yet●MCP — One write-up cut an 85,000-token tool result to 310 tokens by wrapping it in an ADK FunctionTool●FLASH — gemini-3.7-flash and 3.5-flash now route automatically to newer Flash models (10/08)●NEW — Stop Re-Reading Every Crash Report: Gre…●ARGON — gemini-4-argon is generally available as of 10/13, aimed at complex coding, agents, math and science●DEEP RES — The old Deep Research agent shut down on 10/23. If calls fail, move to deep-research-preview-04-2026●NANO — gemini-nano-banana-2.1 is GA (10/06). gemini-3.1-flash-image is deprecated with no shutdown date yet●MCP — One write-up cut an 85,000-token tool result to 310 tokens by wrapping it in an ADK FunctionTool●FLASH — gemini-3.7-flash and 3.5-flash now route automatically to newer Flash models (10/08)●NEW — Stop Re-Reading Every Crash Report: Gre…
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-api290idempotency5cloudflare-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-07-15
Sample what you already accepted — an audit budget that catches silent quality drift
A confidence gate only ever looks at output the model hesitated on. Silent drift sinks into the batch that sailed through. Working from a fixed 30-minutes-a-day review budget, this walks through deriving detection time from the binomial, reallocating the same budget across risk strata, and catching slow decay with a cumulative monitor.
◈ API / SDK2026-07-04
When Gemini API Leaks Japanese Into Your English Output Once in a While — Field Notes on Measuring the Contamination Rate and Tightening It in Stages
You told Gemini to answer in English, and 3 out of 100 runs slip a Japanese sentence into the tail. Here is why you cannot stop that 'once in a while', and a production pattern that measures the contamination rate as an SLO and tightens it with graded recovery, with working code.
◈ API / SDK2026-05-20
Evolving Gemini API Structured Output Schemas in Production — Design Notes from an Indie App
How I rebuilt the JSON contract layer for a Gemini-powered recommendation feature in a long-running indie app — Dual-Emit, Sunset protocol, and a Python compatibility checker.