Apollo Client InMemoryCache serving stale results under default cache-first policy
0 reputation · 19 Jan 2025, 10:24 UTC
0 reputation · 19 Jan 2025, 10:24 UTC
Apollo Client's InMemoryCache uses a cache-first fetch policy by default: when a query already has cached results, reads are served from the normalized store without contacting the server. The cache has no built-in time-to-live, so entries persist indefinitely until a mutation writes new data, refetchQueries runs, or entries are evicted.
The goal is to keep query results acceptably fresh for data that can change on the server independently of any client-side mutation, without pushing every read onto the network. Candidate mechanisms include the cache-and-network policy, refetchQueries on selected mutations, cache.evict followed by cache.gc, and pollInterval, but it is unclear how their freshness guarantees and request costs compare in Apollo Client 3.x.
Specific questions:
33275 reputation · 19 Jan 2025, 21:13 UTC
1. Is there a supported TTL for cache‑first queries? No. Apollo Client 3.x does not expose a time‑to‑live or staleness threshold for the cache-first policy. Cached data remains until a mutation writes new data, refetchQueries is called, or the entry is evicted/garbage‑collected.
2. How do refetchQueries, cache eviction, and pollInterval compare for keeping server‑side data fresh? Each has a different trade‑off profile:
3. Does cache‑first ever revalidate in the background? No. cache-first only falls back to the network on a cache miss. Once a query has a cached result, the client will never trigger a background fetch unless you change the fetch policy or explicitly poll.
Because Apollo’s cache is purely in‑memory and normalized, it has no notion of “age.” The library deliberately keeps data until it is overwritten or cleared. This design keeps the default policy fast and deterministic but requires developers to add freshness logic when needed.
Below are concrete actions you can take, grouped by strategy. Pick the one that best fits your data volatility and network constraints.
@client field __cacheTimestamp that you set to Date.now() whenever you write the entity.read function in typePolicies that checks the timestamp against the current time. If the age exceeds your threshold, return undefined so Apollo falls back to the network.const client = new ApolloClient({
cache: new InMemoryCache({
typePolicies: {
Query: {
fields: {
user: {
read(existing, { readField }) {
const ts = readField('__cacheTimestamp');
if (!ts || Date.now() - ts > MAX_AGE_MS) {
return undefined; // trigger network
}
return existing;
}
}
}
}
}
})
});
refetchQueries After MutationsrefetchQueries: [{ query: OtherQuery }] to the mutation options.client.cache.evict({ id: 'User:123' }) or client.cache.evict({ fieldName: 'users' }) to remove it from the store.client.cache.gc() to clean up any dangling references.pollInterval on QueriespollInterval: 30000 (30 s) to the useQuery hook.refetchQueries or manual TTL.pollInterval for the most critical queries.To fine‑tune the recommendation, could you share what “acceptable staleness” looks like for your app? For example, do you need data to be fresh within 5 minutes, or is a 30‑second window acceptable?
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.