●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
Why Gemini API Returns Empty Responses with finishReason: RECITATION, and the Prompt + Post-Processing Design That Stopped It
Run a Gemini content agent long enough and one day logs fill with finishReason: 'RECITATION' and empty content arrays. This is the verbatim-quotation safety system firing. Here is the prompt rewriting pattern and TypeScript post-processor I deployed across six auto-publishing pipelines at Dolice — it dropped my incident rate by 90%.
Run a Gemini content agent in production for long enough and one day this lands in your logs: finishReason: "RECITATION", status 200 OK, and candidates[0].content.parts is empty. The call looks like a success on the wire, but nothing came back. As an indie developer at Dolice running six auto-publishing pipelines (Claude Lab, Gemini Lab, Antigravity Lab, Rork Lab, plus the older content sites Lacrima and Mystery), I went through a month where this blocked roughly 20 article generations and I kept waking up to short daily output counts.
Recitation block is the safety system that fires when Gemini judges its in-flight response is about to emit a long verbatim quote of copyrighted material. It is independent of safetySettings, so you cannot turn it off — it has to be addressed at both the prompt and the post-processing layer.
Here is the end-to-end design — detector, paraphrase-forced prompt, three-tier fallback, model switch — that I shipped to production, plus the failure rates I measured before and after.
Five conditions that trigger recitation block
Across six months of logs, the trigger patterns settled into five buckets:
Requests that long-quote existing articles, news, or papers (≈45%): "based on the body of this article…", "summarize chapter 3 of this paper…" — anything that nudges the model to reproduce a stretch of the source verbatim
Lyrics, poems, famous literary text (≈20%): even small fragments of copyrighted lyrics or fiction
Long verbatim copies of public codebases (≈15%): asking the model to reproduce an entire framework tutorial as-is
Headline + lead paragraph stacking in news summarizers (≈12%): asking the model to reproduce the headline, dek, and opening graf in sequence
Verbatim re-emission of long structured data (≈8%): "echo back this dataset as JSON" patterns that try to mirror every field
Retry success rates were roughly 5% / 0% / 30% / 10% / 60%. Causes 1–4 are prompt-design problems, not transient errors. Without rewriting the prompt, retries do nothing.
Cleanly detecting the block on response
The API returns HTTP 200 with finishReason: "RECITATION" and an empty parts array. A naive implementation reads it as an empty success.
citationMetadata sometimes contains hints about which public source the response was matching against. I log this so the offending prompt is easy to find and rewrite.
✦
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
✦Recognize the five prompt patterns that trigger Gemini's recitation block, with the API response shape (finishReason + citationMetadata) you need to detect it cleanly
✦Adopt the 'paraphrase-forced prompt' template that took my own incident rate from ~20/month to 1–2/month — over 90% reduction — directly out of a production pipeline
✦Take home the three-tier fallback (retry / paraphrase prompt / chunked re-aggregation) and a model-switch backstop, with cost numbers from six months of production logs
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.
The single change that dropped incidents the most was switching from "use this source" to "use this source, but never quote it directly". The template that has held up across six months:
function buildParaphrasePrompt(sourceText: string, task: string): string { return [ "You are a writer who restructures information. The [SOURCE] below is for", "reference only. Strict constraints:", "", "- Never reuse a sequence of 10 or more consecutive words from the source", "- Paraphrase every sentence (reorder, swap synonyms, restructure)", "- Do not reproduce anything except proper nouns, numbers, and dates", "- When attribution is needed, use indirect speech: 'as the source notes…',", " 'X stated that…' rather than verbatim quotation", "", `TASK: ${task}`, "", "[SOURCE]", sourceText, ].join("\n");}
My pipeline's incident rate went from roughly 20 per month down to 1–2 per month — over 90% reduction. In direct API cost, that meant about 350 fewer retried calls per month and roughly $19/month saved at Gemini 2.5 Pro input/output pricing.
Three-tier fallback
For the residual incidents, I run a three-tier ladder:
Plain retry once (sometimes works for ~5% of cases)
Reissue with the paraphrase-forced prompt (clears another ~70%)
Chunk the source, summarize each chunk, then integrate
async function safeGeminiCall( client: GoogleGenerativeAI, modelId: string, sourceText: string, task: string): Promise<string> { const model = client.getGenerativeModel({ model: modelId }); const initial = await model.generateContent(`${task}\n\n${sourceText}`); let det = detectRecitation(initial.response); if (!det.isRecitation && det.partial) return det.partial; const paraphrasePrompt = buildParaphrasePrompt(sourceText, task); const second = await model.generateContent(paraphrasePrompt); det = detectRecitation(second.response); if (!det.isRecitation && det.partial) return det.partial; return chunkAndAggregate(model, sourceText, task);}async function chunkAndAggregate(model: any, source: string, task: string): Promise<string> { const chunks = splitIntoChunks(source, 3000); const summaries: string[] = []; for (const chunk of chunks) { const prompt = buildParaphrasePrompt(chunk, "extract the three main points of this fragment"); const r = await model.generateContent(prompt); const d = detectRecitation(r.response); if (d.partial) summaries.push(d.partial); } const integration = buildParaphrasePrompt(summaries.join("\n---\n"), task); const final = await model.generateContent(integration); return detectRecitation(final.response).partial;}function splitIntoChunks(text: string, size: number): string[] { const chunks: string[] = []; for (let i = 0; i < text.length; i += size) { chunks.push(text.slice(i, i + size)); } return chunks;}
If even tier 3 fails, the underlying prompt is asking for verbatim reproduction by design — that needs a redesign upstream, not more retries.
Sanitizing the source before it ever reaches Gemini
Better than catching the block is shaping the input so it does not fire. My pipeline runs the source text through a sanitizer first:
interface SanitizationResult { sanitized: string; flags: string[];}function sanitizeSource(source: string): SanitizationResult { const flags: string[] = []; let s = source; // Long quote blocks (more than 30 words inside quotation marks) are risk-prone s = s.replace(/"[^"]{60,}"/g, (match) => { flags.push("long-quote-truncated"); return match.slice(0, 30) + "…[truncated]…" + match.slice(-30); }); // URLs sometimes get treated as quotation triggers; strip to domain s = s.replace(/https?:\/\/([\w.-]+)[^\s]*/g, (_match, host) => `[${host}]`); return { sanitized: s, flags };}
In my logs, sources flagged with long-quote-truncated had a recitation rate 6× higher than unflagged sources. Now any flagged source enters the pipeline already routed to tier 2 (paraphrase prompt) — skipping tier 1 saves both latency and API cost.
Last-ditch model switch
When all of the above still blocks, switching to a different Gemini variant sometimes clears it. I have observed Gemini 2.5 Pro and Gemini 3.2 Pro applying the recitation detector with slightly different sensitivities — the same source can block on one and pass on the other, and the direction is not consistent.
const MODEL_FALLBACK_CHAIN = [ "gemini-3-2-pro", "gemini-2-5-pro", "gemini-2-5-flash",] as const;async function modelFallback( client: GoogleGenerativeAI, sourceText: string, task: string): Promise<string> { for (const modelId of MODEL_FALLBACK_CHAIN) { try { const result = await safeGeminiCall(client, modelId, sourceText, task); if (result) return result; } catch (err) { // continue to next model } } throw new Error("all models blocked by recitation");}
My news-summarization agent was hitting recitation noticeably more on 2.5 Pro than on 3.2 Pro, so I switched that single agent's primary to 3.2 Pro. Block rate fell from around 8% to 0.5%.
Three quirks worth knowing
A few behaviors I did not see in any official documentation:
First, mid-stream recitation blocks still send earlier tokens to the client. The user sees the response start, then it stops with no error visible at the UI level. Always check the final finishReason before flushing the streamed text to the user.
Second, you are still billed for tokens generated up to the block point. The response looks empty but the cost is real. At one point about 4% of my Gemini bill was effectively paying for blocked outputs, and that was the moment I decided the fallback design was worth the engineering.
Third, citationMetadata is present in some blocks and absent in others. Absent-metadata cases are harder to debug and tend to recur on the same prompt — so they get fast-tracked to a prompt rewrite in my workflow.
Recitation block is one of those quiet, persistent production costs in Gemini that does not show up until you watch your logs carefully. I hope sharing six months of iteration helps anyone hitting the same wall.
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.