GEMINI LABJP
ROBOTICS — The ER 1.6 preview that shut down on August 31 does have a successor. Gemini Robotics ER 2 is in public preview, in both standard and streaming variantsVIDEO — ER 2 judges success and failure from live video rather than still snapshots, which is what lets it catch spills, slips, and misalignments while a task is still runningDEADLINE — Next up is September 30, when gemini-omni-flash-preview is retired. The target is gemini-omni-1.1-flash, GA since August 27, and there are now under four weeks leftAPIKEY — Every remaining standard API key, restricted ones included, stops working during September. The replacement is an auth key bound to a Google Cloud service accountPRICE — Gemini 3.7 Flash keeps its introductory $0.75/$3.75 per 1M through December 31, then moves to $1.50/$7.50 on January 1, 2027. Any estimate crossing the year needs both figuresAUDIO — Gemini 3.5 Transcribe handles language detection across 85+ languages, speaker diarization, word-level timestamps, and custom vocabulary biasing of up to 1,000 termsROBOTICS — The ER 1.6 preview that shut down on August 31 does have a successor. Gemini Robotics ER 2 is in public preview, in both standard and streaming variantsVIDEO — ER 2 judges success and failure from live video rather than still snapshots, which is what lets it catch spills, slips, and misalignments while a task is still runningDEADLINE — Next up is September 30, when gemini-omni-flash-preview is retired. The target is gemini-omni-1.1-flash, GA since August 27, and there are now under four weeks leftAPIKEY — Every remaining standard API key, restricted ones included, stops working during September. The replacement is an auth key bound to a Google Cloud service accountPRICE — Gemini 3.7 Flash keeps its introductory $0.75/$3.75 per 1M through December 31, then moves to $1.50/$7.50 on January 1, 2027. Any estimate crossing the year needs both figuresAUDIO — Gemini 3.5 Transcribe handles language detection across 85+ languages, speaker diarization, word-level timestamps, and custom vocabulary biasing of up to 1,000 terms
Articles/API / SDK
API / SDK/2026-04-29Advanced

Production Streaming UI with Gemini API + TanStack Query — Cancellation, Retries, and Cache Coherence

TanStack Query is optimized for one-shot REST/JSON requests, so streaming responses don't fit naturally. This guide walks through the gotchas of using Gemini API SSE with TanStack Query and the production-grade design patterns that hold up in real apps.

Gemini API230TanStack QueryReact2Streaming5Production33

Premium Article

Production Streaming UI with Gemini API + TanStack Query — Cancellation, Retries, and Cache Coherence

Have you ever started writing an AI chat with useMutation thinking "TanStack Query is great, I'll just use it for state too," and then hit a wall the moment streaming kicked in? I did. Showing partial chunks on the screen was easy enough, but then I tripped over every production landmine in turn: duplicate streams when users hammered the send button, retry storms after tab focus, half-rendered text after errors.

TanStack Query is optimized for the "send a request, get JSON back" model. Streaming responses (SSE or ReadableStream) live outside that model, and naive integration falls apart. This article shares the patterns I converged on while shipping Gemini-powered chat UIs to production — with copy-pasteable code.

Why TanStack Query and Streaming Don't Mix Naturally

TanStack Query handles caching, deduplication, and refetching for a given key. Its mental model assumes "one request, one response." Streaming is fundamentally different: chunks arrive over time, the stream might be cancelled, and the final result is the concatenation of chunks rather than the last one.

If you ignore this difference and try to fit streaming into useQuery, you'll watch select re-render the whole tree every chunk, and staleTime semantics will collapse. The conclusion I arrived at: handle the stream itself with useMutation, and only sync the finalized result into the query cache via queryClient.setQueryData.

Conceptually:

  • User submits → mutate() is called
  • Inside mutationFn, consume the ReadableStream chunk by chunk
  • Push partial text into local React state for live display
  • On completion, call queryClient.setQueryData(['conversation', id], full)
  • Other components read history through useQuery

With this split, you keep TanStack Query's strengths (caching, invalidation, optimistic updates) while letting plain React state handle the streaming-specific concerns.

Server Side: A Next.js Route Handler That Streams Gemini SSE

Let's start on the server. I'm using Next.js App Router with Node.js runtime — Edge runtime can hit constraints with certain SDK features, so it's safer to start with Node and migrate later if needed.

// app/api/chat/stream/route.ts
import { GoogleGenerativeAI } from "@google/generative-ai";
 
export const runtime = "nodejs";
export const dynamic = "force-dynamic"; // disable caching
 
const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY!);
 
export async function POST(req: Request) {
  const { messages, conversationId } = await req.json();
 
  // Detect client disconnects
  const abortSignal = req.signal;
 
  const model = genAI.getGenerativeModel({
    model: "gemini-2.5-flash",
    generationConfig: { temperature: 0.7, maxOutputTokens: 2048 },
  });
 
  const encoder = new TextEncoder();
  const stream = new ReadableStream({
    async start(controller) {
      try {
        const result = await model.generateContentStream({
          contents: messages.map((m: { role: string; content: string }) => ({
            role: m.role === "assistant" ? "model" : "user",
            parts: [{ text: m.content }],
          })),
        });
 
        for await (const chunk of result.stream) {
          // Bail immediately if the client disconnected
          if (abortSignal.aborted) {
            controller.close();
            return;
          }
          const text = chunk.text();
          if (!text) continue;
          // SSE format: "data: <JSON>\n\n"
          const payload = JSON.stringify({ type: "chunk", text });
          controller.enqueue(encoder.encode(`data: ${payload}\n\n`));
        }
        // Completion signal
        controller.enqueue(
          encoder.encode(`data: ${JSON.stringify({ type: "done", conversationId })}\n\n`)
        );
        controller.close();
      } catch (err: unknown) {
        const message = err instanceof Error ? err.message : "stream error";
        controller.enqueue(
          encoder.encode(`data: ${JSON.stringify({ type: "error", message })}\n\n`)
        );
        controller.close();
      }
    },
  });
 
  return new Response(stream, {
    headers: {
      "Content-Type": "text/event-stream",
      "Cache-Control": "no-cache, no-transform",
      Connection: "keep-alive",
      "X-Accel-Buffering": "no", // disables intermediate proxy buffering
    },
  });
}

Three details matter here. First, checking req.signal inside the loop lets the server close its Gemini API connection the moment the client disconnects. Without that check, the server keeps reading the entire stream even after the user closes the tab — wasted tokens, wasted money. Second, errors are reported as SSE events with type: "error" rather than HTTP status codes, because once headers are flushed you can't change the response code, and a typed error event is much easier to handle on the client. Third, the X-Accel-Buffering: no header disables buffering at intermediate proxies — without it, Cloudflare or Nginx may hold your "stream" for several seconds and deliver it all at once.

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
Anyone stuck wiring useMutation to a ReadableStream will leave today with a complete, working implementation that handles cancellation, retries, and partial-result preservation
You'll learn how to control the duplicate streams and memory leaks that appear when users switch tabs and refocus mid-stream, by combining queryClient with AbortController correctly
Even when users hammer the send button or connections drop, your UI will stay intact, partial replies will be preserved, and your server-side token spend will drop noticeably
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 $15 for lifetime access
View Membership →

Related Articles

API / SDK2026-08-23
Streaming Gemini TTS: concatenate the PCM, write the WAV header once
Streamed Gemini TTS does not arrive as an audio file. It arrives as raw PCM fragments. Here is what happens when you wrap each fragment in its own WAV header, measured on my machine, plus the receiving code that avoids it.
API / SDK2026-08-07
When Streaming Responses Quietly Lose Non-ASCII Text: Measuring the Byte Boundary and the Event Boundary Separately
I measured why Japanese text disappears from streaming Gemini responses using a local mock SSE server. Two separate layers were broken, one of them silently, and an English-only test suite caught neither.
API / SDK2026-06-28
When Gemini × Qdrant Hybrid Search Was Quietly Losing Recall — Field Notes on Instrumenting RRF Weights and Sparse-Vector Drift
Run Gemini embeddings with Qdrant hybrid search in production and your dashboards stay green while recall quietly slips. These field notes show how to catch it with measurement — RRF weights, sparse-vector drift, missing payload indexes — and protect it with a quality budget.
📚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 →