This episode covers persisting the cache to localStorage or IndexedDB using the sync-storage and async-storage persisters, as well as offline behavior: onlineManager, exponential backoff retry, and handling disconnected networks.

Modern web applications must be resilient on bad networks. When a user opens the application without a connection, the TanStack Query cache that only lives in memory will be empty, and the page will show a never-ending loading state. The solution: persistence — saving the cache to durable storage such as localStorage or IndexedDB, so data is available when the application is opened again.
Episode 12 covers two things: how to save the cache with a persister, and how TanStack Query behaves when the network disconnects — including onlineManager and retry with increasing delays.
TanStack Query provides two official persisters: query-sync-storage-persister for localStorage and query-async-storage-persister for async storage such as IndexedDB.
npm install @tanstack/react-query-persist-client @tanstack/query-sync-storage-persisternpm install @tanstack/react-query-persist-client adds PersistQueryClientProvider, and the second persister package provides createSyncStoragePersister.
After installing, replace QueryClientProvider with PersistQueryClientProvider:
import { PersistQueryClientProvider } from "@tanstack/react-query-persist-client"
import { createSyncStoragePersister } from "@tanstack/query-sync-storage-persister"
const queryClient = new QueryClient({
defaultOptions: {
queries: {
staleTime: 5 * 60 * 1000,
gcTime: 1000 * 60 * 60 * 24,
},
},
})
const persister = createSyncStoragePersister({
storage: window.localStorage,
})
function App() {
return (
<PersistQueryClientProvider
client={queryClient}
persistOptions={{ persister }}
>
<Todos />
</PersistQueryClientProvider>
)
}createSyncStoragePersister uses window.localStorage as the storage medium. Note the extended gcTime — a persisted cache is only restored if it hasn't passed gcTime, so set gcTime longer than a short session. PersistQueryClientProvider automatically saves the cache to localStorage and restores it when the application is opened.
localStorage is limited to about 5 MB and is synchronous. For large caches, use the async persister with IndexedDB:
import { createAsyncStoragePersister } from "@tanstack/query-async-storage-persister"
import { IDBStorage } from "idb-keyval"
const persister = createAsyncStoragePersister({
storage: IDBStorage,
})createAsyncStoragePersister works with async Storage implementations like idb-keyval. IDBStorage from idb-keyval stores data asynchronously, which is better suited for large caches because it doesn't block the main thread.
When the network disconnects, failed queries don't give up immediately. TanStack Query uses retry with increasing delays — exponential backoff. Each failed attempt increases the delay before the next attempt, up to the retry limit.
useQuery({
queryKey: ["todos"],
queryFn: fetchTodos,
retry: 3,
retryDelay: (attempt) => Math.min(1000 * 2 ** attempt, 30 * 1000),
})retryDelay: (attempt) => Math.min(1000 * 2 ** attempt, 30 * 1000) produces delays of 1 second, 2 seconds, 4 seconds, then caps at 30 seconds. retryDelay accepts a function to compute the delay per attempt — a built-in exponential backoff pattern without any extra library.
TanStack Query tracks the online status through onlineManager. When the connection returns, failed queries that are still within their retry window are automatically retried:
import { onlineManager } from "@tanstack/react-query"
onlineManager.setOnline(navigator.onLine)
window.addEventListener("offline", () => onlineManager.setOnline(false))
window.addEventListener("online", () => onlineManager.setOnline(true))onlineManager.setOnline tells the library the current connection status. onlineManager internally decides when retries resume — when online is set back to true, pending requests are woken up again. By default, TanStack Query uses the browser's built-in online/offline events.
For queries that genuinely shouldn't be retried — for example a wrong user input — shrink the retry value or use the error status to show a message immediately. A common combination: retry: 1 for read queries, and the default retry for idempotent operations.
Warning
Sensitive data persisted to localStorage can be read by other scripts on the same origin. Don't persist tokens or personal data without security consideration — episode 15 will discuss this practice more deeply.
Episode 12 made your application resilient: a cache that survives through localStorage or IndexedDB, reasonable offline behavior, and retry with exponential backoff controlled by onlineManager. This combination keeps the application useful even when the network isn't friendly.
Key takeaways:
PersistQueryClientProvider saves the cache to persistent media.gcTime so the cache can be restored across sessions.onlineManager controls behavior when the connection returns.In the next episode, episode 13, we will discuss performance and cache tuning — reducing excessive fetches with the right staleTime, keepPreviousData, and cache normalization, plus Suspense mode with useSuspenseQuery for granular loading integrated with React Suspense.