What this usually means
Apollo's normalized cache relies on type names and IDs to merge objects. If the mutation response doesn't include a matching `__typename` and `id` (or custom keyFields), Apollo can't link the new data to existing cache entries. Alternatively, the mutation modifies a field that's part of a list query, but you didn't tell Apollo to update that query via `refetchQueries` or an `update` function. A third common pattern: you used `cache.writeQuery` or `cache.modify` incorrectly, creating a partial or orphaned cache entry.
The first ten minutes — establish facts before touching code.
- 1Open Apollo Client DevTools (or Redux DevTools with Apollo plugin). Check the ROOT_QUERY and the normalized objects for the affected type.
- 2Run the mutation in Apollo Studio with the same variables and headers. Confirm the response has `__typename` and the expected ID field.
- 3Add a `console.log` inside the mutation's `update` function. Verify it's called and inspect the cache read/write.
- 4Temporarily add `refetchQueries: [YOUR_QUERY]` to the mutation. If it works, you have a cache update strategy issue.
- 5Check if the query uses `fetchPolicy: 'cache-only'` or `'no-cache'` — these skip cache updates.
The specific files, logs, configs, and dashboards that usually own this bug.
- searchMutation definition: check the response shape and whether `__typename` is included.
- searchCache configuration: `typePolicies` in `InMemoryCache` constructor for custom key fields.
- searchApollo Client DevTools: 'Cache' tab – inspect the objects under the type name.
- searchNetwork tab: compare the mutation response JSON with the query cache objects.
- searchQuery component: look at `fetchPolicy`, `nextFetchPolicy`, and `errorPolicy`.
- searchUpdate function: the mutation's `update` callback (if used) – check for bugs in `cache.readQuery` and `cache.writeQuery`.
- searchServer code: ensure the mutation resolver returns all fields the client expects, especially IDs.
Practical causes, not theory. These are the things you will actually find.
- warningMissing `__typename` or `id` in the mutation response – Apollo can't normalize.
- warningUsing `cache.writeQuery` with an incomplete or mismatched query shape.
- warningThe mutation modifies a field that's part of a paginated list, but you didn't handle pagination in the update function.
- warningCustom `keyFields` in cache config don't match the actual data – duplicate or missing keys.
- warningThe query uses `fetchPolicy: 'network-only'` but the cache is still stale because the mutation result isn't merged into the existing query.
- warningRace condition: the mutation completes, but the query is still in flight and overwrites the cache with old data.
Concrete fix directions. Pick the one that matches your root cause.
- buildEnsure the mutation response includes `id` (or your custom key) and `__typename` for every object.
- buildUse `refetchQueries` or `await refetchQueries()` for simple cases where you want to re-run affected queries.
- buildWrite a proper `update` function that reads the current cache, modifies the list, and writes back using `cache.writeQuery`.
- buildFor nested or computed fields, use `cache.modify` with field-level policies to merge data correctly.
- buildSet `typePolicies` for the type to define a custom merge function for lists (e.g., `merge: (existing, incoming) => incoming` to replace).
- buildUse `@client` directives for local-only fields to avoid cross-query pollution.
A fix you cannot prove is a guess. Close the loop.
- verifiedAfter fix, run the mutation and immediately inspect the cache via DevTools – the query should show updated data.
- verifiedAdd a `console.log` inside the query component's render to see the data change without a refresh.
- verifiedWrite an integration test that mocks the mutation and verifies the query cache updates.
- verifiedCheck that `cache.gc()` or cache eviction doesn't remove the updated data (if you call it manually).
- verifiedTest with a slow network (throttle) to confirm no race condition between query and mutation responses.
Things that make this bug worse or harder to find.
- warningUsing `cache.writeData` (deprecated) instead of `cache.writeQuery`.
- warningCalling `client.resetStore()` or `client.clearStore()` in the mutation's `onCompleted` – this wipes the entire cache.
- warningAssuming `refetchQueries` is always the best solution – it wastes bandwidth and can cause UX flicker.
- warningForgetting to include the mutation's own data in the `update` function when you need to merge it.
- warningOverwriting the entire cache with `cache.writeQuery` without reading existing data first – this causes data loss.
- warningMismatching variable names between the mutation and the update function's query – Apollo will silently fail to write.
Users List Not Updating After CreateUser Mutation
Timeline
- 09:00Report: new users don't appear in the list without a page refresh.
- 09:10Check network tab: mutation returns 200 with user data (id, name, email).
- 09:15Open Apollo DevTools: ROOT_QUERY.users is unchanged. Mutation data appears under User type.
- 09:20Review mutation code: no `update` function, no `refetchQueries`. Relies on cache normalization.
- 09:25Check Users query: uses `fetchPolicy: 'cache-and-network'` and no keyArgs.
- 09:30Inspect cache config: no typePolicies for User. Default keyFields = ['id'].
- 09:35Compare mutation response: includes `__typename: 'User'` and `id`. Cache should merge.
- 09:40Closer look: mutation response has field `__typename: 'User'` but query returns `User` objects with `__typename: 'User'`. No mismatch.
- 09:45Check the server resolver: it returns the created user, but the list query uses a different resolver that returns a different set of fields (e.g., missing `email`).
- 09:50Root cause: the list query asks for `id`, `name`, `email`; mutation returns `id`, `name`, `email`, `role`. The cache merges correctly, but the list query's `users` field is a separate list that doesn't include the new user because the mutation didn't touch that list.
- 10:00Fix: add `refetchQueries: ['GetUsers']` to the mutation. Test: new user appears immediately.
I got a Slack ping at 9 AM: 'New users not showing up in the admin panel unless you refresh.' Classic cache staleness. I opened the network tab first, ran the CreateUser mutation, and saw a 200 with the user object – id, name, email, all there. So the backend was fine. I opened Apollo DevTools and saw that the ROOT_QUERY.users array hadn't changed. But the normalized User cache had the new user object. That meant Apollo recognized the type and ID, but it didn't magically insert that object into the list query.
I checked the mutation code. The developer had skipped the `update` function entirely and didn't add `refetchQueries`. They assumed Apollo would automatically add the new user to the list because the types matched. That's a common misunderstanding – Apollo's cache normalization merges objects by ID, but it doesn't automatically add items to lists. You have to either refetch the list or manually update it via `cache.modify`.
I added `refetchQueries: ['GetUsers']` to the mutation options. That forced Apollo to re-run the Users query after the mutation completed. The list updated immediately. The lesson: know the difference between cache normalization (which merges individual objects) and cache updates for lists. Use `refetchQueries` for simplicity, or write an `update` function for better performance and UX.
Root cause
Mutation had no `update` function and no `refetchQueries`, so the list query cache was never told about the new user.
The fix
Added `refetchQueries: ['GetUsers']` to the mutation call. Alternatively, implemented an `update` function using `cache.modify` to append the new user to the list.
The lesson
Apollo does not automatically update list queries after a mutation that creates an item of the same type. You must explicitly tell it to update the list via `refetchQueries` or a manual `update` function.
Apollo Client normalizes every object that has a `__typename` and an ID field (default `id`). It stores them in a flat key-value store. Queries are stored as references to these normalized objects. When a mutation returns an object with the same `__typename` and ID as an existing cache object, Apollo will merge the fields. But if the query is a list (e.g., `users`), the list itself is stored as an array of references. The mutation does not automatically add the new object's reference to that array. You must update the list manually.
Check your cache config. If you use custom `keyFields` (e.g., `keyFields: ['slug']`), the mutation must return that field. Otherwise, Apollo creates a new cache entry with a different key, and the query's reference points to the old, unmerged object. Use Apollo DevTools to inspect the cache keys – they look like `TypeName:id`.
The `update` function receives the current cache and the mutation result. A common mistake is using `cache.readQuery` with variables that don't match the original query, causing a silent miss. Always pass the exact same variables that the query used. Another pitfall: mutating the array returned by `cache.readQuery` directly – you must create a new array. Example: `const data = cache.readQuery({ query: GET_USERS }); const newUsers = [...data.users, newUser]; cache.writeQuery({ query: GET_USERS, data: { users: newUsers } });`.
For paginated lists, use `cache.modify` with field policies. Define a `merge` function in `typePolicies` that concatenates incoming items with existing ones. But be careful: if you use `fetchMore`, the merge function is called for pagination, so you need to differentiate between initial load and append. Use `args` like `offset` or `limit` in `keyArgs` to control cache behavior.
If your component has a query that refetches automatically (e.g., `refetch` on focus, or `pollInterval`), it might fetch before the mutation's cache update is applied. The fetched data replaces the cache, including your manual update. To avoid this, use `fetchPolicy: 'network-only'` for the query after mutation, or use `await refetchQueries()` in the mutation's `onCompleted` to ensure the query runs after the mutation.
Another scenario: optimistic updates. If you implement optimistic UI, ensure the optimistic response matches the real response shape. If they differ, Apollo may not reconcile properly, leaving stale data. Always test optimistic updates with a slow network simulator.
Apollo DevTools is your best friend. Open it, go to the Cache tab, and look for your type (e.g., `User`). You'll see all normalized objects. Check if the mutation result created a new object or updated an existing one. If you see two objects with the same ID but different fields, your merge strategy is broken.
For custom key fields, verify that the key you defined matches the data. For example, if you set `keyFields: ['username']` but the mutation returns `userName` (different case), Apollo will treat them as separate objects. Use the `possibleTypes` option if your schema uses unions or interfaces – Apollo needs to know the concrete types to normalize correctly.
Frequently asked questions
Why does `refetchQueries` work but cache update doesn't?
`refetchQueries` re-runs the query from the server, which always returns fresh data. If your `update` function is not working, it's likely a bug in how you read or write the cache. Common issues: mismatched query variables, missing fields in the write, or modifying the cache object in place instead of creating a new reference.
How do I update a list cache after a delete mutation?
Use `cache.modify` to remove the item from the list. Inside the `update` function, call `cache.modify({ fields: { users: (existingUserRefs, { readField }) => existingUserRefs.filter(ref => readField('id', ref) !== deletedUserId) } })`. Alternatively, use `refetchQueries` for simplicity.
What is the difference between `cache.writeQuery` and `cache.writeFragment`?
`cache.writeQuery` writes to a specific query root (like `ROOT_QUERY`). It's used to update the data for a specific query. `cache.writeFragment` writes to a normalized object directly, using its typename and ID. Use `writeFragment` when you want to update a single object's fields without affecting the query that fetched it.
Can I use `client.resetStore()` to fix cache staleness?
You can, but it's a sledgehammer. It clears the entire cache and refetches all active queries. This can cause a flash of empty UI and unnecessary network requests. Only use it as a last resort or if you need to clear all cache (e.g., after logout). For targeted updates, prefer `refetchQueries` or manual `update` functions.
Why does my mutation update the cache but the UI doesn't re-render?
Apollo React components re-render when the data they subscribe to changes. If the cache update doesn't trigger a re-render, the component might not be subscribed to the exact field that changed. Check if the component uses a different query than the one you updated. Also, ensure the component is not using `React.memo` with a stale closure, or that the query variables haven't changed.