Choosing an Apollo Client fetchPolicy: cache-first vs network-only vs cache-and-network
A decision guide for Apollo Client fetchPolicy: compare cache-first, network-only, cache-and-network, and no-cache against freshness, load, and offline needs.
19 Nov 2025, 02:01 UTC

The decision you actually have to make
Every query in Apollo Client runs under a fetchPolicy, and the default (cache-first) is not always the right one. Pick wrong and you either show stale data after a mutation, hammer your GraphQL server with redundant requests, or break your app the moment the user goes offline. The useful rule of thumb: choose the policy based on how fresh the data must be and whether the screen must work without a network, not based on habit.
This guide assumes Apollo Client 3.x with the default InMemoryCache. Behavior described here is stable across the 3.x line, but confirm against your installed version.
The supported options, compared
| Policy | Reads cache? | Always hits network? | Writes to cache? | Best for |
|---|---|---|---|---|
cache-first (default) | Yes, first | No — only on cache miss | Yes | Relatively static data: product catalogs, user profiles, config |
cache-and-network | Yes, first | Yes | Yes | Frequently changing data where instant render matters: feeds, dashboards |
network-only | No | Yes | Yes | Data that must be fresh and is shared with other queries via the cache |
no-cache | No | Yes | No | Sensitive or one-off data you never want normalized into the cache |
(standby and cache-only also exist but are niche: cache-only never touches the network, and standby behaves like cache-first without reacting to cache updates. They rarely drive an architecture decision.)
Trade-offs that bite in production
cache-first and staleness. The cache is only as fresh as your normalization and mutation updates. If a mutation modifies an entity and you neither return the updated object from the mutation nor call cache.evict/cache.modify, cache-first will keep serving the old object indefinitely. This is the single most common Apollo bug report pattern.
network-only and server load. Every component mount that executes the query produces an HTTP request. On a list screen with polling or frequent remounts, this multiplies traffic and can hit rate limits. If you need freshness but not on every render, consider cache-first plus refetch() on user action, or nextFetchPolicy to downgrade after the first load.
cache-and-network and UI flicker. The hook first returns cached data with networkStatus indicating a refetch is in flight, then re-renders with the server result. If the two payloads differ meaningfully (reordered lists, deleted rows), users see content jump. Design list components to handle this — for example, key rows by stable ID and animate removals — or reserve this policy for data where diffs are small.
no-cache and lost conveniences. Because the response is never written, normalized updates from mutations won't reach components using this policy, and you lose deduplication. Use it deliberately (e.g., a one-time export preview), not as a default.
A concrete implementation
Set the policy per query where the requirement lives, and use nextFetchPolicy to avoid paying the network cost on every remount:
// In your React component (client-side, no special permissions needed)
const { data, loading, networkStatus, refetch } = useQuery(ORDERS_QUERY, {
// First mount: always get fresh data
fetchPolicy: 'cache-and-network',
// Subsequent remounts within the session: trust the cache
nextFetchPolicy: 'cache-first',
notifyOnNetworkStatusChange: true, // so `networkStatus` updates on refetch
});
// Show a subtle spinner only for background refetches, not the initial load
const isRefetching = networkStatus === NetworkStatus.refetch;For data that must never be cached — say, a query returning a short-lived token — be explicit:
const { data } = useQuery(SIGNED_URL_QUERY, {
fetchPolicy: 'no-cache',
});You can also set a client-wide default, but prefer per-query choices:
new ApolloClient({
cache: new InMemoryCache(),
defaultOptions: {
watchQuery: { fetchPolicy: 'cache-and-network' },
},
});How to verify the policy is doing what you think
Don't trust the configuration — observe the behavior:
- Browser Network tab: with
network-onlyorno-cache, you should see an HTTP request on every query execution. Withcache-first, remount the component after a successful load — no new request should appear unless the cache entry was evicted. - Apollo Client DevTools (Cache tab): after a
cache-firstorcache-and-networkquery, the normalized entities should be present. After ano-cachequery, they should not. - cache-and-network double render: add a temporary
console.log(data, networkStatus)in the component. On a warm cache you should see two renders: cached data with a refetch status, then the fresh result. - Staleness check: run a mutation that changes the data without updating the cache, then re-render a
cache-firstquery. If it still shows old values, you've confirmed the invalidation gap described above — fix it by returning updated fields from the mutation or usingcache.modify.
Limitations to keep in mind
Fetch policies govern the read path only; they don't replace a cache invalidation strategy. Pagination with fetchMore interacts with policies in non-obvious ways (merge functions in type policies matter more). And none of these policies give you true offline support on their own — that requires a persisted cache (e.g., apollo3-cache-persist), at which point cache-first and cache-and-network become the only sensible choices.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.