Storage Persisters
Purpose and Scope
Storage persisters connect TanStack Query persistence to a concrete storage API. Persistence itself is the workflow that serializes a dehydrated query client, saves it outside memory, restores it later, and removes it when the cache should be discarded. This page compares the synchronous and asynchronous storage persister packages, explains the public options they accept, and gives practical guidance for choosing a storage target. It is focused on the React plugin docs because the examples use @tanstack/react-query-persist-client, but the persister contract returned by both factories is the shared Persister shape used by the persistence client core.
Sources: docs/framework/react/plugins/createSyncStoragePersister.md, docs/framework/react/plugins/createAsyncStoragePersister.md, packages/query-sync-storage-persister/src/index.ts, packages/query-async-storage-persister/src/index.ts
The important product direction is that createSyncStoragePersister is deprecated. The sync docs explicitly state that the plugin will be removed in the next major version and that users can simply use @tanstack/query-async-storage-persister instead. That recommendation is supported by the async docs: an async persister accepts storage APIs whose methods may return values or promises, and the docs note that synchronously reading and writing storage, such as browser localStorage, also satisfies the async storage interface. New integrations should therefore start with the async package unless they are maintaining existing sync-persister code.
Sources: docs/framework/react/plugins/createSyncStoragePersister.md, docs/framework/react/plugins/createAsyncStoragePersister.md
Relevant Source Files
docs/framework/react/plugins/createSyncStoragePersister.mddocuments installation, usage, deprecation status, retry behavior, and the option interface for the sync storage persister.docs/framework/react/plugins/createAsyncStoragePersister.mddocuments installation, React Native style usage, the async storage interface, async retry behavior, and default options for the async persister.packages/query-sync-storage-persister/src/index.tsimplementscreateSyncStoragePersister, including default values, serialization, retry loops, no-op SSR behavior, and a timer-based throttle.packages/query-async-storage-persister/src/index.tsimplementscreateAsyncStoragePersister, including promise-aware serialize, deserialize, storage access, retry handling, andasyncThrottleintegration.packages/query-sync-storage-persister/src/__tests__/storageIsFull.test.tsverifies basic sync persistence and the storage-quota retry path usingremoveOldestQuery.packages/query-async-storage-persister/src/__tests__/asyncThrottle.test.tsverifies async throttling behavior, including coalescing calls, long-running functions, thrown errors, and error callbacks.
Choosing Between Sync and Async Storage
Use @tanstack/query-async-storage-persister for new code. It can target asynchronous storage such as React Native AsyncStorage, IndexedDB wrappers that expose a compatible API, or custom storage services that return promises. It can also target browser storage objects whose methods complete synchronously, because its AsyncStorage contract permits MaybePromise return values for getItem, setItem, and removeItem. In practice, that makes the async persister the more future-facing default: it covers mobile, browser, and custom persistence without tying the persistence lifecycle to synchronous storage calls.
Sources: docs/framework/react/plugins/createAsyncStoragePersister.md, packages/query-async-storage-persister/src/index.ts
Use @tanstack/query-sync-storage-persister only when maintaining existing integrations that already depend on it. Its implementation expects a storage object with synchronous getItem, setItem, and removeItem methods returning immediately, and its public docs mark the plugin as deprecated. The implementation still has useful behavior: it supports window.localStorage and window.sessionStorage, accepts undefined or null storage for server-side rendering or constrained WebViews, and returns no-op methods when no storage object is supplied. Those traits make old browser integrations stable, but they should not be a reason to choose the deprecated package for fresh work.
Sources: docs/framework/react/plugins/createSyncStoragePersister.md, packages/query-sync-storage-persister/src/index.ts
The storage decision should follow the runtime rather than the UI framework. For React web apps, localStorage or sessionStorage can be enough for small persisted caches, but storage quota is limited and all values are strings. For React Native, the docs show @react-native-async-storage/async-storage with PersistQueryClientProvider, because persistence must restore before regular query activity resumes. For larger payloads, a custom async storage layer is usually easier to evolve because serialization and writes can be promise-based, compressed, encrypted, or backed by a database-like browser API.
Sources: docs/framework/react/plugins/createAsyncStoragePersister.md, docs/framework/react/plugins/createSyncStoragePersister.md
Installation and Basic Usage
Both storage persisters are separate packages and are used with @tanstack/react-query-persist-client. The async package is installed as @tanstack/query-async-storage-persister; the sync package is installed as @tanstack/query-sync-storage-persister. The docs pair each persister package with the React persist client because the persister only knows how to save, restore, and remove a serialized client. The provider or persistQueryClient function coordinates that persister with a QueryClient, including when restore happens and which cache instance is being persisted.
Sources: docs/framework/react/plugins/createAsyncStoragePersister.md, docs/framework/react/plugins/createSyncStoragePersister.md
npm install @tanstack/query-async-storage-persister @tanstack/react-query-persist-client
pnpm add @tanstack/query-async-storage-persister @tanstack/react-query-persist-client
yarn add @tanstack/query-async-storage-persister @tanstack/react-query-persist-client
bun add @tanstack/query-async-storage-persister @tanstack/react-query-persist-clientA typical async setup creates a QueryClient, creates a persister from a storage object, and passes the persister through PersistQueryClientProvider. The docs example sets query gcTime to 24 hours, which is a practical reminder that persisted cache entries are still governed by TanStack Query cache semantics. Persistence saves the dehydrated cache, but it does not make expired or garbage-collected data permanently fresh. Choose gcTime, stale settings, and persistence max-age policy together so the restored experience matches the product's offline or return-visit expectations.
Sources: docs/framework/react/plugins/createAsyncStoragePersister.md
import AsyncStorage from '@react-native-async-storage/async-storage'
import { QueryClient } from '@tanstack/react-query'
import { PersistQueryClientProvider } from '@tanstack/react-query-persist-client'
import { createAsyncStoragePersister } from '@tanstack/query-async-storage-persister'
const queryClient = new QueryClient({
defaultOptions: {
queries: {
gcTime: 1000 * 60 * 60 * 24,
},
},
})
const persister = createAsyncStoragePersister({
storage: AsyncStorage,
})API Components
Both factory functions return a Persister with three operations: persistClient, restoreClient, and removeClient. persistClient saves a PersistedClient after serializing it; restoreClient reads the configured key and deserializes the stored value; removeClient deletes the stored key. The default key is REACT_QUERY_OFFLINE_CACHE, the default throttle interval is 1000 milliseconds, the default serializer is JSON.stringify, and the default deserializer is JSON.parse. These defaults appear in both docs and implementations, making the package surfaces intentionally parallel.
Sources: docs/framework/react/plugins/createSyncStoragePersister.md, docs/framework/react/plugins/createAsyncStoragePersister.md, packages/query-sync-storage-persister/src/index.ts, packages/query-async-storage-persister/src/index.ts
| Component | Package | Storage shape | Retry shape | Status |
|---|---|---|---|---|
createAsyncStoragePersister | @tanstack/query-async-storage-persister | `AsyncStorage | undefined | null` |
createSyncStoragePersister | @tanstack/query-sync-storage-persister | `Storage | undefined | null` |
The async option interface is promise-aware in more places. Its serialize option may return a string or a promise for a string, deserialize may return a PersistedClient or a promise, and retry may asynchronously produce a replacement client or undefined. The storage interface accepts getItem, setItem, and removeItem, plus an optional entries method. This does not mean every integration must be asynchronous; it means the async persister can safely await the storage boundary and therefore works with both simple and more capable backends.
Sources: docs/framework/react/plugins/createAsyncStoragePersister.md, packages/query-async-storage-persister/src/index.ts
The sync option interface is narrower. Its serialize function must immediately return a string, deserialize must immediately return a PersistedClient, and storage writes are wrapped in a synchronous trySave function. If setItem throws, the persister enters a retry loop. Each retry receives the attempted client, the error, and an incrementing errorCount; if the retryer returns another client, the persister attempts to save that new client. If the retryer returns undefined, the loop stops and no further persistence attempt is made for that call.
Sources: docs/framework/react/plugins/createSyncStoragePersister.md, packages/query-sync-storage-persister/src/index.ts
Retries, Quotas, and Throttling
Storage writes can fail for ordinary reasons: a browser quota may be exceeded, a mobile storage implementation may reject a write, or custom serialization may throw. The docs define retryers as a way to recover gracefully by transforming the attempted persisted client. The predefined strategy highlighted in the sync docs is removeOldestQuery, imported from the persist client core package, which removes the oldest query and returns a smaller PersistedClient. The sync storage quota test builds a mock storage limit, persists several large queries, uses removeOldestQuery, and verifies that the restored cache contains fewer queries after retry succeeds.
Sources: docs/framework/react/plugins/createSyncStoragePersister.md, packages/query-sync-storage-persister/src/tests/storageIsFull.test.ts
Throttling protects storage from excessive writes. The sync implementation keeps the latest arguments and schedules one delayed write with timeoutManager.setTimeout; repeated calls during the wait window update the saved parameters rather than creating many storage writes. The async implementation delegates to asyncThrottle with the configured interval. Its tests demonstrate that closely spaced calls are coalesced so the latest call is executed, that the minimum interval is respected even when the function itself takes longer than the interval, and that a thrown error does not prevent a later invocation from running.
Sources: packages/query-sync-storage-persister/src/index.ts, packages/query-async-storage-persister/src/index.ts, packages/query-async-storage-persister/src/tests/asyncThrottle.test.ts
A useful rule is to treat throttleTime as part of the durability contract. A shorter interval reduces the amount of cache state that can be lost if the app is closed immediately after a query update, but it increases write pressure and may expose quota or latency problems sooner. A longer interval reduces writes but makes persistence more eventual. For most applications the default 1000 millisecond value is a reasonable starting point; adjust it only after observing storage size, write frequency, and restore requirements for the target runtime.
Sources: docs/framework/react/plugins/createSyncStoragePersister.md, docs/framework/react/plugins/createAsyncStoragePersister.md, packages/query-sync-storage-persister/src/index.ts, packages/query-async-storage-persister/src/index.ts
Implementation Details and Edge Cases
Both implementations intentionally tolerate missing storage. The option comments call out server-side rendering and Android WebViews: for SSR, pass undefined; in some WebView configurations, window.localStorage can be null. In those cases, the sync persister returns no-op persistClient, restoreClient, and removeClient functions. The async persister also returns no-op persistence and removal, while restoreClient resolves to undefined. This behavior lets application setup create a persister conditionally without crashing in environments where persistent storage is not available.
Sources: packages/query-sync-storage-persister/src/index.ts, packages/query-async-storage-persister/src/index.ts
Serialization is another boundary to design deliberately. The docs mention that browser storage has size limits and that serialize and deserialize can be overridden to compress or decompress persisted data. That hook is also the natural place for application-specific transformations, provided the output remains compatible with the storage value type expected by the persister. For sync storage, the transformation must be immediate. For async storage, the transformation may await work, which is useful when compression, encryption, or storage preparation is asynchronous.
Sources: docs/framework/react/plugins/createSyncStoragePersister.md, docs/framework/react/plugins/createAsyncStoragePersister.md, packages/query-sync-storage-persister/src/index.ts, packages/query-async-storage-persister/src/index.ts
Testing Signals and Next Steps
The tests emphasize behavior that users depend on rather than only type shape. The sync storage test verifies that a dehydrated client containing multiple query result types can be stored and restored exactly, then separately verifies quota recovery by shrinking the persisted client with removeOldestQuery. The async throttle tests cover timing edge cases and failure recovery, including a historical bug scenario where call timing and long execution could otherwise break scheduling. These signals support the guidance to rely on built-in throttling and retry hooks instead of wrapping the persister in separate ad hoc write queues.
Sources: packages/query-sync-storage-persister/src/tests/storageIsFull.test.ts, packages/query-async-storage-persister/src/tests/asyncThrottle.test.ts
Next, read the Persist Query Client page to understand how PersistQueryClientProvider, persistQueryClient, busters, restore timing, and cache hydration fit around the persister. If you are building a React Native or offline-first application, start with createAsyncStoragePersister, choose a storage implementation, set an appropriate gcTime, and add a retry strategy if persisted payload size can exceed storage limits. If you are migrating old browser code, replace createSyncStoragePersister with the async package first, then revisit custom serialization and throttling after confirming restore behavior.
Sources: docs/framework/react/plugins/createAsyncStoragePersister.md, docs/framework/react/plugins/createSyncStoragePersister.md