cswork / libraryOpen practice workspace

Articles

TanStack Query Interview Questions: Cache Lifetimes, Refetching, and Optimistic Updates

Practice TanStack Query interview questions with a tested request timeline, staleTime vs gcTime, query keys, refetch triggers, and optimistic rollback.

TanStack Query interview questions become easier when you separate three things: whether cached data is fresh, whether anybody is observing it, and which event can request new data. staleTime answers the first question. gcTime controls cleanup after a query becomes unused. Neither setting is a polling interval.

The practical challenge is predicting behavior across mounting, returning to a tab, invalidating data, and handling a failed optimistic update. This guide gives you a request-count exercise and an inventory mutation you can explain line by line.

Evidence boundary: API rules come from official TanStack Query v5 documentation. Our original experiments ran against @tanstack/query-core 5.102.8. They count controlled query-function calls and exercise cache observers; they do not measure HTTP traffic or browser rendering. Interview prompts below are editorial practice, not candidate-reported questions from a particular employer.

Start with the two cache lifetimes

Freshness means the client can reuse a result without the ordinary stale-data refetch triggers. Retention means the result still exists in memory. A result can be stale and retained, fresh and retained, or absent after garbage collection. Treating all three situations as “expired” loses information you need to predict the UI.

The official defaults distinguish staleTime from gcTime. With ordinary configuration, stale data can refetch on mount, refocus, or reconnect. Unused queries are retained for five minutes by default. A query that still has an observer is not deleted simply because five minutes pass.

For an interview example, say: “I set freshness to thirty seconds because a short-lived inventory view can tolerate that delay. I retain unused data for several minutes so returning navigation can display a cached result immediately.” Those are separate product decisions, even if you happen to choose the same duration.

Do not infer server truth from freshness. A fresh cached stock value can already differ from the database after another customer purchases an item. Freshness is a client policy; the server still validates the purchase.

Make query keys describe the response

In v5, use an object argument and a top-level array key:

useQuery({
  queryKey: ['inventory', { warehouseId, availability }],
  queryFn: () => fetchInventory(warehouseId, availability),
  staleTime: 30_000,
  gcTime: 5 * 60_000,
})

The query-key guide explains that keys identify cached data and should include variables used by the query function. If the warehouse changes but the key does not, you have described two different responses as one cache entry.

Object-property order within a key is hashed deterministically; array-item order matters. The useful interview principle is not “serialize everything.” It is “include the inputs that distinguish this response, using a stable, serializable structure.” Avoid a new timestamp in every key unless each timestamp really represents different data.

Two components with the same key can share a cache entry. They do not automatically mean two independent copies of server data. However, observer-specific options can differ, so ask about each component's configuration before promising an exact request count. Our exercise deliberately gives both observers the same options.

Predict the request-count timeline

Here is the original controlled experiment. We disable retries, set staleTime to 100 milliseconds and gcTime to 180 milliseconds, and use a query function that increments a counter. These short durations make a local demonstration quick; they are not production recommendations.

EventTotal callsExplanation
Observer A subscribes with no cached result1Initial fetch supplies data.
Observer B subscribes while that result is fresh1Same key and fresh data are reused.
Freshness duration passes, with no other event1Becoming stale is not polling.
We send a false-to-true focus signal2The stale observed query refetches.
We invalidate the active inventory query3Active matching data is refreshed.
Both unsubscribe; a fresh remount occurs before cleanup3Cached fresh data is still present.
The last observer leaves; cleanup finishes; a new observer joins4The cache entry is gone, so data must be fetched.

The actual fixture asserted every count and checked that getQueryData returned undefined after cleanup. We waited beyond timing boundaries rather than relying on an exact scheduler tick.

In a live interview, identify whether a request is already in flight, whether data exists, whether it is stale, and whether a trigger is enabled. State those assumptions before counting. Development tools, retries, focus changes, and differing observers can otherwise make a confident number wrong.

Distinguish focus, polling, and invalidation

The focus-refetching documentation describes background refresh when a user returns to stale data. The browser's default integration listens for visibility changes. Our core test calls focusManager.setFocused directly, so it verifies the response to a focus signal, not the browser event listener itself.

Polling uses refetchInterval. If the product needs updates every ten seconds while a screen stays open, discuss that option explicitly. Increasing staleTime does not create an update schedule, and reaching its boundary does not mean a timer must call your endpoint.

Invalidation marks matching queries stale and, by default, refetches active matches in the background. An inactive query can be invalidated without an immediate request. This distinction matters after editing a detail page when a filtered list is currently unmounted.

Prefix matching lets ['inventory'] target related keys, while exact matching narrows the operation. Explain why you chose the scope. Refreshing every inventory list is simple but may be unnecessary; refreshing only one detail can leave a mounted list inconsistent.

Explain Infinity and static without overpromising

Two v5 configurations deserve separate answers. staleTime: Infinity prevents ordinary time-based staleness, but manual invalidation can still cause refresh. The stricter 'static' setting prevents the normal invalidation-driven and automatic refetch paths documented for static queries.

Do not summarize either setting as “network requests can never happen.” Explicit operations and application changes still need to be considered. The interview point is that 'static' is intended for data you deliberately treat as unchanging during the relevant runtime, while Infinity still supports an invalidation strategy.

A permissions response is a good design discussion: if authorization can change during a session, freezing it indefinitely may be a poor product decision. Client caching also never replaces authorization checks on the server. Explain the consequence of the setting instead of memorizing a special string.

The v5 migration guide also explains the rename from cacheTime to gcTime and the single-object API. When an interviewer provides older code, identify its version before treating a former API as universally invalid.

Repair an optimistic inventory update

Optimistic updates change the UI before the server confirms a mutation. Our exercise starts with stock 5 in both an inventory list and item 7's detail entry. A reservation displays 4 immediately. The server rejects it, so both entries must return to 5.

The official optimistic-update guide describes canceling relevant outgoing queries, taking a snapshot, applying a provisional update, rolling back on error, and invalidating afterward. Each step addresses a specific failure.

Canceling an older refetch prevents its result from overwriting the provisional value. A snapshot gives rollback an actual previous value rather than a guessed inverse operation. Updating both entries keeps two visible representations consistent. Invalidation finally reconciles with the authoritative server state.

Our tested callback sequence was:

onMutate: async () => {
  await client.cancelQueries({ queryKey: ['inventory'] })
  const beforeList = client.getQueryData(listKey)
  const beforeDetail = client.getQueryData(detailKey)
  client.setQueryData(listKey, rows =>
    rows.map(row => row.id === 7 ? { ...row, stock: 4 } : row))
  client.setQueryData(detailKey, row => ({ ...row, stock: 4 }))
  return { beforeList, beforeDetail }
},
onError: (_error, _variables, previous) => {
  client.setQueryData(listKey, previous.beforeList)
  client.setQueryData(detailKey, previous.beforeDetail)
},
onSettled: () =>
  client.invalidateQueries({ queryKey: ['inventory'] })

These callbacks assume the two entries already exist, as they do in the fixture. Production code needs an explicit policy for absent data. It should also derive the stock change from the mutation variables instead of hardcoding 4.

The experiment observed [4, 4] inside the rejected mutation and [5, 5] after rollback. Its cached entries were inactive, so settlement invalidation did not prove a new HTTP request. That is a useful distinction to include in your answer.

Discuss the concurrency trap in snapshot rollback

A single-mutation example does not solve concurrent edits. Imagine reservation A saves a snapshot of 5 and displays 4. Reservation B then displays 3. If A fails and restores its whole snapshot, it can erase B's still-valid optimistic change.

The correct response depends on the product. You might serialize mutations for the same item, keep pending operations separately and derive the displayed value, or rebase remaining operations over a confirmed server response. A versioned server contract can help detect conflicts, but it does not remove the need to define UI behavior.

Say which guarantee your implementation actually provides. “This rollback handles one pending reservation per item” is clearer than claiming the sample is concurrency-safe. Add a second overlapping mutation to the test before making that stronger claim.

Also distinguish mutation failure from a stale read after success. A server can accept a write while a replica temporarily returns older data. Cache callbacks alone do not define backend consistency. If asked, connect your frontend strategy to the endpoint's documented response and consistency guarantees.

Verify the explanation with observable evidence

A useful test records query-function calls, cache values, and observer status separately. Do not assert only the final rendered number: the UI may look correct while making unnecessary requests or leaving a neighboring cache entry stale.

For timing tests, keep retries disabled and control the clock or leave generous timing margins. Restore focus-manager state and clear the client afterward so one test cannot influence another. Our standalone fixture uses short real delays and a fresh client; a larger automated suite would benefit from a controlled timer implementation.

The retry documentation distinguishes client retry defaults from server execution. Therefore, explicitly setting retry: false is part of our experiment's contract, not a claim about every deployment.

For a browser-level follow-up, open two components sharing a key, record network requests, hide and reveal the tab, then compare results with the core fixture. That would test the integration layer we have not measured here. Keeping those layers separate makes your evidence easier to trust.

Practice the related frontend decisions

QuestionWhat to practice
Implement, Debug, and Optimize a React TableSeparate rendering problems from data-fetch behavior.
Implement React logic with TypeScript hooksExplain dependencies and state transitions.
Build a Paginated Provider Search Backend and React Filter UIIdentify parameters that belong in query keys.
Implement a React CRUD TableKeep list operations and mutations coherent.
Debug and optimize React performance issuesUse measurements before changing behavior.

A strong TanStack Query interview answer predicts a sequence rather than reciting defaults. Name the key, freshness policy, observers, trigger, and expected call count. For mutations, add the provisional value, rollback boundary, and reconciliation step.

Sources and Further Reading