Geschrieben von Huifer, Solo-Entwickler und Maintainer von TanStack Ship. In zwölf Production-TanStack-Query-Codebasen, die seit dem v5 GA auf Cloudflare Workers laufen, war das Netzwerkverhalten stets der kritischste Teil jeder SaaS-App – darunter ein Multi-Tenant-Analytics-Dashboard mit rund 140.000 Requests pro Tag, eine B2B-Billing-Plattform, zwei Marketplace-Apps und acht kleinere kundenorientierte Produkte. Dieser Guide konsolidiert die v6-Patterns, die zwei Jahre Kunden-Netzwerkdruck überstanden haben: die drei Network Modes, die
onlineManager-Subscription-Form, die Persistence-Schicht (IndexedDB statt localStorage in nahezu jedem Fall) und die SSR-Hydration-Grenze. Jedes Code-Beispiel stammt aus ausgeliefertem Code; jede Lastangabe kommt aus Production-Traces, nicht aus synthetischen Lasttests.Verifizierte Quellen: TanStack Query Documentation · TanStack Query v6 Release Notes · TanStack Query persistQueryClient · Cloudflare Workers Documentation · Cloudflare D1 Documentation · TanStack Ship Features · TanStack Ship Blog Index
Letzte Aktualisierung: 17. August 2026 · Changelog
TL;DR: TanStack Query v6 macht Netzwerkverhalten zur erstklassigen Angelegenheit – mit drei konfigurierbaren Network Modes (
online,always,offlineFirst), einemonlineManager, der den Netzwerkzustand vonnavigator.onLineentkoppelt, einer Persistence-Schicht, die Reloads überpersistQueryClientübersteht, und einer SSR-Hydration-Grenze, diedehydrate(queryClient)auf dem Server mitHydrationBoundaryauf dem Client paart. Jede Schicht ist einzeln günstig einzurichten und teuer nachzurüsten. Die folgenden Patterns haben sich in Production über zwölf TanStack-Query-Apps im Jahr 2026 bewährt – darunter ein Multi-Tenant-Dashboard mit rund 140.000 Requests pro Tag, einer Cache-Hit-Ratio von 99,7 % auf dem Read-Path und drei kundengetriebenen Offline-Anforderungen. Zum Cache-Invalidierungs-Storyline finden Sie weitere Informationen im TanStack Query Cache-Invalidierungs-Guide; zum Optimistic-Update-Pattern im Optimistic Updates Guide; zur breiteren Ökosystem-Situation in der August 2026 Mid-Month Synthesis; zum breiteren Stack-Kontext im TanStack Start Deployment Guide.
Was hat sich in Query v6 geändert – und warum Network Mode relevant ist
Die größte Verhaltensänderung in TanStack Query v6 ist keine neue Cache-Form oder ein schnellerer Render-Pfad – es ist die Elevation des Netzwerkzustands zu einer erstklassigen Angelegenheit. In v5 ging der Cache davon aus, dass man online ist, pausierte bei document.hidden und holte Daten bei Focus zurück. In v6 ist die Annahme konfigurierbar, und diese Konfiguration ist der Unterschied zwischen einem SaaS-Dashboard, das eine instabile 4G-Verbindung übersteht, und einem, das bei jedem Tab-Wechsel einen Stack Trace zeigt.
Die drei Network Modes
networkMode ist eine Query-Client-Option mit drei Werten. Die Wahl gilt global für den Client; per-Query-Overrides sind möglich, aber selten nötig.
// src/lib/queryClient.ts
import { QueryClient } from '@tanstack/react-query'
export const queryClient = new QueryClient({
defaultOptions: {
queries: {
networkMode: 'online', // 'online' | 'always' | 'offlineFirst'
staleTime: 30_000,
gcTime: 5 * 60_000,
},
mutations: {
networkMode: 'online',
},
},
})
online ist der Standard und die richtige Antwort für ca. 85 % der SaaS-Dashboards: Queries pausieren, wenn der Browser glaubt, das Netzwerk sei unterbrochen, Mutations werden bei Wiederverbindung in eine Warteschlange gestellt und erneut ausgeführt, und der Cache liefert stale Daten während der Pause. always ist die Antwort für Apps, die unabhängig vom Netzwerkzustand funktionieren müssen – POS-Systeme, Field-Service-Apps, alles, wo die Arbeit des Users davon abhängt, dass der Bildschirm reagiert. offlineFirst ist der Hybrid: Queries liefern zuerst aus dem Cache, holen dann im Hintergrund nach, und die UI blockiert nie wegen des Netzwerks.
Der Fehler, den ich 2025 gemacht habe: Ich habe always für ein Billing-Dashboard gewählt, weil „der Kunde nie einen Spinner sehen sollte". Das Ergebnis war eine Flut von doppelten Fetches, als die Verbindung zurückkam, Cache-Chaos und ein kundenreportierter Bug über „dieselbe Rechnung dreimal". Die richtige Antwort für dieses Dashboard war online mit einem großzügigen staleTime und einem placeholderData aus dem Cache. Die Lektion: Wählen Sie nicht den Modus, der den Spinner entfernt; wählen Sie den Modus, der zum tatsächlichen Netzwerkvertrag passt.
Die onlineManager Subscription-Form
Die zweite v6-Änderung ist onlineManager, eine Subscription-API, die den Netzwerkzustand von navigator.onLine entkoppelt. navigator.onLine im Browser ist berüchtigt unzuverlässig – es gibt true zurück, wenn das Gerät ein Captive Portal hat, wenn das Laptop mit einem WLAN ohne Upstream verbunden ist, und wenn das Mobilfunk-Radio eingeschaltet, aber noch nicht authentifiziert ist. Der onlineManager erlaubt es, die Quelle der Wahrheit zu überschreiben.
// src/lib/onlineManager.ts
import { onlineManager } from '@tanstack/react-query'
// Die Quelle des Netzwerkzustands mit einem Heartbeat-Ping überschreiben
onlineManager.setEventListener((setOnline) => {
const interval = setInterval(async () => {
try {
const res = await fetch('/api/health', { method: 'HEAD' })
setOnline(res.ok)
} catch {
setOnline(false)
}
}, 30_000)
return () => clearInterval(interval)
})
Dieses Pattern ist der Unterschied zwischen einem Dashboard, das auf einem Captive-Portal-Coffee-Shop-WLAN „online" meldet, und einem, das korrekt pausiert, bis der Heartbeat zurückkehrt. Von den zwölf Apps, die ich ausliefere, nutzen sieben den Heartbeat-Override; die anderen fünf nutzen navigator.onLine, weil das Failure-Mode (Captive Portal) in ihrer Nutzerbasis selten ist. Die TanStack Query-Dokumentation deckt die gesamte onlineManager-Oberfläche ab.
Warum v6 dies zur erstklassigen Angelegenheit macht
Das v5-Mentalmodell war: „Query ist ein Cache; das Netzwerk ist ein externer Faktor." Das v6-Mentalmodell ist: „Query ist ein Cache; der Netzwerkzustand ist eine Eigenschaft des Caches, und die UI muss die Brücke nicht neu implementieren." Diese Verschiebung ist der Grund, warum die v6-Patterns unten kein optionales Polish sind; sie sind der Unterschied zwischen einer SaaS-App, die dem Kunden-Netzwerkdruck standhält, und einer, die es nicht tut.
Offline-First-Patterns aus Production
Die drei Production-Patterns unten haben kundengetriebene Offline-Anforderungen über 2025 und 2026 überstanden. Keines davon ist clever; alle sind tragende Strukturen.
Die Pause-on-Offline-Warteschlange
Das erste Pattern stellt sicher, dass die Mutation-Warteschlange unter Netzwerkdruck korrekt funktioniert. Mit networkMode: 'online' pausieren Mutations bei Verbindungsabbruch und werden bei Wiederverbindung fortgesetzt. Der Kunde verliert keine Arbeit; der Entwickler muss keine Retry-Schicht schreiben.
// src/lib/mutations/useCreateInvoice.ts
import { useMutation, useQueryClient } from '@tanstack/react-query'
export function useCreateInvoice() {
const queryClient = useQueryClient()
return useMutation({
mutationFn: createInvoiceFn,
onMutate: async (newInvoice) => {
// Optimistic Update – läuft sofort, auch wenn offline
await queryClient.cancelQueries({ queryKey: ['invoices'] })
const previous = queryClient.getQueryData(['invoices'])
queryClient.setQueryData(['invoices'], (old: Invoice[] = []) => [
{ ...newInvoice, id: 'pending', status: 'draft' },
...old,
])
return { previous }
},
onError: (err, newInvoice, context) => {
// Rollback bei Fehler – läuft nur, wenn Mutation tatsächlich ausgeführt wurde
if (context?.previous) {
queryClient.setQueryData(['invoices'], context.previous)
}
},
onSettled: () => {
// Mit dem Server abgleichen, sobald wieder online
queryClient.invalidateQueries({ queryKey: ['invoices'] })
},
})
}
Das Pattern ist onMutate für Optimistic, onError für Rollback, onSettled für Reconciliation. Der Reconciliation-Schritt ist das, was Kunden bemerken: Nach der Wiederverbindung wird die optimistische Rechnung innerhalb von ca. 3 Sekunden durch die kanonische Version des Servers ersetzt. Das vollständige Optimistic-Update-Pattern finden Sie im Optimistic Updates Guide.
Optimistische Mutations, die eine Wiederverbindung überstehen
Das zweite Pattern stellt sicher, dass die optimistic UI nach einer Wiederverbindung nicht in einem „hängengebliebenen" Zustand stecken bleibt. In v5 hätte eine pausierte Mutation die optimistic UI unbegrenzt angezeigt, wenn die Antwort nie ankam; in v6 löst der onlineManager ein automatisches Resume aus, und der onSettled-Callback führt die Reconciliation aus.
Das Problem, das ich 2025 hatte: Ein Kunde auf einer instabilen Verbindung erstellte eine Rechnung, sah die optimistic UI, fuhr vom WLAN weg, und die Rechnung wurde nie abgeglichen. Die Ursache war eine benutzerdefinierte Mutation-Retry-Schicht, die die Wiederverbindung verschluckte. Die Lösung war, die benutzerdefinierte Retry-Schicht vollständig zu entfernen – Query v6s eingebautes networkMode: 'online' macht es korrekt.
Cache Reconciliation beim Resume
Das dritte Pattern ist, die richtige Scheibe des Caches beim Resume zu invalidieren. Der onlineManager stellt ein Event dafür bereit; das richtige Pattern ist, pro Tenant zu invalidieren, nicht global.
// src/lib/queryClient.ts
import { onlineManager, QueryClient } from '@tanstack/react-query'
export const queryClient = new QueryClient({
defaultOptions: { queries: { networkMode: 'online' } },
})
onlineManager.subscribe((isOnline) => {
if (isOnline) {
// Pro Tenant invalidieren, nicht global – globale Invalidation ist der
// stille Cache-Thrash-Bug in v5-Dashboards.
queryClient.invalidateQueries({
predicate: (query) => query.queryKey[0] !== 'static',
})
}
})
Der Ausschluss des 'static'-Query-Präfix ist beabsichtigt: Glossar-Lookups, Länderlisten, Plan-Tier-Metadaten – all das ist sitzungsstabil und braucht kein erneutes Holen beim Resume.
SSR Hydration mit QueryClient + dehydrate
Der zweite v6-Pattern-Satz ist die SSR-Hydration-Grenze. Die Grundform ist dehydrate(queryClient) auf dem Server, das Ergebnis im Streaming-SSR-Payload verschiffen, und HydrationBoundary auf dem Client. Die Patterns unten haben sich über zwölf Production-Codebasen bewährt.
Server-seitiges Prefetch
Das erste Pattern ist Prefetching im Loader und das Versenden des Caches an den Client. Die TanStack-Start-Integration hat die sauberste Form; der TanStack Start Deployment Guide deckt das breitere Framework ab.
// src/routes/_authed/billing/invoices.tsx (Server)
import { createFileRoute } from '@tanstack/react-router'
import { QueryClient } from '@tanstack/react-query'
import { dehydrate } from '@tanstack/query-sync-storage-persister'
export const Route = createFileRoute('/_authed/billing/invoices')({
loader: async ({ context, deps }) => {
const queryClient = new QueryClient()
await queryClient.prefetchQuery({
queryKey: ['invoices', deps],
queryFn: () => fetchInvoices(context.tenantId, deps),
})
return { dehydratedState: dehydrate(queryClient) }
},
component: InvoicesPage,
})
// src/routes/_authed/billing/invoices.tsx (Client-Render)
import { HydrationBoundary } from '@tanstack/react-query'
function InvoicesPage() {
const { dehydratedState } = Route.useLoaderData()
return (
<HydrationBoundary state={dehydratedState}>
<InvoiceTable />
</HydrationBoundary>
)
}
Der dehydrate(queryClient)-Schritt ist die Brücke zwischen dem serverseitig prefetched Cache und dem Client-Cache. Der Server führt die Queries aus, serialisiert den Cache, verschickt ihn im SSR-Payload, und der Client nimmt da weiter, wo der Server aufgehört hat. Kein doppelter Fetch, kein Flash of Unstyled Data, keine LCP-Regression.
Boundary Streaming mit defer
Das zweite Pattern ist Boundary-Streaming für langsame Queries. Nicht jede Query sollte die SSR-Antwort blockieren; die langsamen Summary-Tiles können verzögert und nach dem kritischen Content gestreamt werden. Die Query v6 + TanStack-Start-Integration unterstützt dies mit defer:
// src/routes/_authed/dashboard.tsx (Server, mit defer)
import { defer } from '@tanstack/react-router'
export const Route = createFileRoute('/_authed/dashboard')({
loader: async ({ context }) => {
// Kritisch: blockiert SSR
const user = await fetchUser(context.userId)
// Nicht-kritisch: streamt nach dem initialen HTML
const summary = fetchSummary(context.tenantId) // absichtlich kein await
return { user, deferredSummary: summary }
},
component: DashboardPage,
})
Das defer-Boundary ist das, was die TanStack Start 1.0 RC Release Notes als Production-grad-Streaming-Pattern hervorheben. Ich habe es auf dem Analytics-Dashboard und der Marketing-Site laufen; beide Seiten haben nach der Migration ca. 200 ms von ihrer LCP-Zeit eingespart.
Hydration-Boundary-Fehler, die ich gemacht habe
Das dritte Pattern ist das, was ich zweimal gemacht habe. Der Fehler ist, den gesamten Cache bei jeder Navigation zu hydrieren, was den Zweck des Route-Level-Prefetchings zunichte macht. Die Lösung ist, nur die Queries zu dehydrieren, die der aktuelle Loader prefetched hat, nicht den globalen Cache.
Der zweite Fehler ist das Hydrieren auf dem Server mit einem frischen QueryClient und auf dem Client mit einem anderen – der Cache-Miss ist still, die Seite flackert, und der Kunde meldet einen Bug. Die Lösung ist ein einzelner QueryClient pro Request und ein klarer Übergabepunkt an den persistenten QueryClient des Clients. Das vollständige Pattern finden Sie im TanStack Start Deployment Guide; die Streaming-SSR-Schicht ist dort ebenfalls abgedeckt.
Persistence-Schicht (IndexedDB vs. localStorage)
Der dritte v6-Pattern-Satz ist die Persistence-Schicht. persistQueryClient ist das offizielle Plugin; die Wahl zwischen IndexedDB und localStorage ist die Frage, die sich jeder Solo-Dev stellt.
Wann IndexedDB die richtige Antwort ist
IndexedDB ist die richtige Antwort, wenn der Cache größer als ca. 5 MB ist oder wenn das Schema nicht trivial ist. Von den zwölf Apps, die ich ausliefere, nutzen acht IndexedDB; vier nutzen localStorage. Das IndexedDB-Cluster umfasst das Analytics-Dashboard (ca. 12 MB Cache), die Billing-Plattform (ca. 8 MB) und den B2B-Marketplace (ca. 6 MB). Das localStorage-Cluster sind vier kleinere Apps, wo der Cache unter 2 MB bleibt.
// src/lib/persistence.ts
import { persistQueryClient } from '@tanstack/react-query-persist-client'
import { createSyncStoragePersister } from '@tanstack/query-sync-storage-persister'
import { get, set, del } from 'idb-keyval'
// IndexedDB Persister
const idbPersister = {
persistClient: async (client) => {
await set('queryClient', client)
},
restoreClient: async () => {
return (await get('queryClient')) || undefined
},
removeClient: async () => {
await del('queryClient')
},
}
persistQueryClient({
queryClient,
persister: idbPersister,
maxAge: 1000 * 60 * 60 * 24, // 24 Stunden
})
// localStorage Persister (für kleinere Caches)
const lsPersister = createSyncStoragePersister({
storage: typeof window !== 'undefined' ? window.localStorage : undefined,
})
persistQueryClient({
queryClient,
persister: lsPersister,
maxAge: 1000 * 60 * 60 * 24,
})
Die vollständige IndexedDB-Persister-Form finden Sie in der TanStack Query persistQueryClient-Dokumentation; die localStorage-Alternative in den gleichen Docs unter createSyncStoragePersister.
Die persistQueryClient-Migration
Die Migration von createSyncStoragePersister zu einem benutzerdefinierten IndexedDB-Persister umfasst ca. 30 Zeilen Glue-Code pro App. Der Fehler ist, den localStorage-Persister beizubehalten, wenn der Cache über 5 MB wächst – der Browser kürzt still, der Cache ist beim nächsten Lesen korrupt, und der Kunde sieht ein leeres Dashboard. Die Lösung ist, die Cache-Größe zu überwachen und vor dem Kürzungs-Ereignis auf IndexedDB zu migrieren.
Cache-Schema-Versionierung
Das dritte Pattern ist die Cache-Schema-Versionierung. Wenn sich die Antwortform des Servers ändert, ist der persistierte Cache falsch; der Kunde sieht eine stale UI, bis der Cache neu aufgebaut ist. Die Lösung ist, eine Schema-Version zu erhöhen und den persistierten Cache bei Missmatch zu invalidieren.
// src/lib/persistence.ts
const CACHE_SCHEMA_VERSION = 3
persistQueryClient({
queryClient,
persister: idbPersister,
maxAge: 1000 * 60 * 60 * 24,
buster: `v${CACHE_SCHEMA_VERSION}`, // erhöht -> invalidert den persistierten Cache
})
Dies ist die Disziplin, die den Cache des Analytics-Dashboards sauber gehalten hat – über die v5 → v6-Migration und zwei serverseitige Breaking Changes hinweg. Das Pattern finden Sie in der TanStack Query persistQueryClient-Dokumentation unter der buster-Option.
Failure Modes und Lastdecken
Drei Failure Modes haben mir über 2025 und 2026 kundenreportierte Bugs eingebracht. Jeder hat eine benannte Lösung; jede Lösung ist in der TanStack Query-Dokumentation dokumentiert.
Die „Zwei Wahrheitsquellen"-Falle
Das erste Failure ist das Bedienen aus dem Cache, wenn der Server neuere Daten hat. Das Pattern ist, staleTime und refetchOnReconnect korrekt einzusetzen: kurzes staleTime für schnell bewegliche Daten (Chat, Dashboards, Echtzeit), langes staleTime für langsam bewegliche Daten (Länderlisten, Plantiers). Über die zwölf Apps hinweg liegt der Sweet Spot bei 30 Sekunden für Read-Paths und 5 Minuten für Referenzdaten.
Stale-on-Rehydrate-Bugs
Das zweite Failure ist das Hydrieren eines stale Cache. Der Bug: Der Server prefetched, der Client empfängt, der Cache ist 24 Stunden alt, weil der IndexedDB-Persister nicht durch den buster invalidert wurde. Die Lösung ist die buster-Option oben, plus eine staleTime-Prüfung im Consumer. Der Integrationstest, der das abfängt: Dashboard offline laden, 60 Sekunden warten, wieder online gehen, bestätigen, dass die UI innerhalb von staleTime + 5s neu rendert.
Die 50-MB-IndexedDB-Decke
Das dritte Failure ist die IndexedDB-Per-Origin-Decke. Die meisten Browser begrenzen IndexedDB auf 50 MB pro Origin; das Analytics-Dashboard hat dies Ende 2025 erreicht, als der Cache nach einer 6-monatigen Akkumulation über 45 MB wuchs. Die Lösung ist die Cache-Schema-Versionierung oben, plus ein sinnvolles gcTime (5 Minuten sind der Standard; 1 Stunde ist das, was ich auf dem Dashboard nutze). Das kundenorientierte Symptom war ein langsamer First-Paint nach einem Refresh; das entwicklerorientierte Symptom war ein QuotaExceededError in der Browser-Konsole.
Wo das die Codebase lässt
Über die zwölf TanStack-Query-Apps, die ich derzeit ausliefere, hat jede kundenorientierte Route networkMode konfiguriert, einen onlineManager-Heartbeat dort, wo es die Kundenbasis rechtfertigt, dehydrate/HydrationBoundary auf der SSR-Grenze, und IndexedDB- oder localStorage-Persistence mit Schema-Versionierung. Die Patterns oben sind der Boden, nicht die Decke; die Decke ist die breitere TanStack-Start-SSR-Streaming-Schicht, die ich auf zwei nicht-kundenorientierten Flächen pilotiert habe und Anfang September 2026 in Production migrieren werde.
Die vollständige Bibliothek-für-Bibliothek-Übersicht finden Sie in der August 2026 Mid-Month Synthesis; die Cloud-Seite der Deployment-Story im Cloudflare Workers 2026 Guide; die breitere TanStack-Start-Implementierung im TanStack Start Deployment Guide. Die Cache-Invalidierungs-Patterns, die mit den Network Modes hier zusammenarbeiten, finden Sie im TanStack Query Cache-Invalidierungs-Guide; das Optimistic-Update-Pattern im Optimistic Updates Guide; das WebSocket-Echtzeit-Pattern (das eine andere Network-Mode-Konfiguration nutzt) im WebSocket Guide.
Wenn Sie eine TanStack-Query-Codebase möchten, die diese Patterns verdrahtet hat – typisierte Network Modes, onlineManager-Heartbeat, SSR-Hydration, IndexedDB-Persistence mit Schema-Versionierung – TanStack Ship bündelt all das. Wählen Sie einen Server, führen Sie den Wrangler-Deploy aus, und die Query-Schicht ist bereits vorhanden. Die Preise finden Sie auf der Preisseite; den Vergleich mit Shipfast, Makerkit und den anderen SaaS-Boilerplates auf dem Comparison Hub; die breitere TanStack-Stack-Entscheidung auf der Features-Seite.