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-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 API192TanStack QueryReact2Streaming3Production32

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 $10 for lifetime access
View Membership →

Related Articles

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.
API / SDK2026-06-26
When your Gemini API spend cap trips, paying users go down too — isolating the blast radius with per-tier projects
A Project Spend Cap stops the entire project at once. To keep a runaway free tier from taking paying users down with it, this is a design note on isolating the cap's blast radius across per-tier projects and closing the ~10-minute delay with an application-side soft budget gate.
API / SDK2026-06-23
Your Gemini API Average Latency Looks Great — But Some Users Still Get Stuck. Defending p95/p99
Your average TTFT is fast, yet a fraction of users keep hitting frozen responses. That is a tail-latency problem (p95/p99). From measurement to model routing, streaming budgets, cache accounting, and retry design — here are the defenses that actually held up in production, with code.
📚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 →