Query executed multiple times

Hi, I’m using useSuspenseQuery to retrieve some data. The code is something like this:

import QUERY from './query.gql'

const Content = ({ id }: { id: string }) => {
  // This comes from a standard react context, I have 2 apollo clients
  const client = useApolloV2Client()

  const { data } = useSuspenseQuery(QUERY, {
    variables: { id },
    client,
    fetchPolicy: 'cache-first',
  })

  return null
}

export const MyWidget = ({ id }: { id: string }) => (
  <ErrorBoundary>
    <Suspense fallback={<Skeleton />}>
      <Content id={id} />
    </Suspense>
  </ErrorBoundary>
)

What I noticed is that the http request is being executed 2 times, with these timestamps for the executions: 2025-11-09T20:37:25.369Z and 2025-11-09T20:37:25.850Z.
The variables are exactly the same and are primitive values, no ref that could change during rerenders. The component that renders MyWidget is not rerendering and the problem happens if Content returns null too.
How is it possible? Shouldn’t request deduplication avoid this?

Some more context: I’m using react 19 and nextjs app router. But the app router is not being used currently, the page is rendered with @loadable/component so it should execute on the client as a normal SPA.

Is there any behaviour related to suspense that could cause multiple executions or even loops? I also have logs of requests being executed thousands of times, like a loop

Phew, that’s a very specific setup you have there, so I can only really try to give an educated guess here :slight_smile:

My first instinct was that maybe your useApolloV2Client returns two different ApolloClient instances at two different moments in time? Generally, React won’t persist state until a component has fully mounted for the first time after being suspended, so if the useApolloV2Client also uses some kind of state, that might be a reason for what you’re seeing and your client is getting recreated in some way.

Also.. just making sure - this is not really Apollo Client in version 2, right? :slight_smile:

As for the bundling situation… I’m having a hard time understanding what’s happening there. Next.js builds very particular bundles, with multiple levels of server-component-rendering, server-side rendering and maybe even partial prerendering, so it’s hard for me to imagine what’s actually going on here. Could you provide some minimal reproduction in some way?

Yes, unfortunately I have a few migrations pending and the setup is not optimal.

The part of different client instances is interesting, I thought something similar and tried to use the exported instance directly without using the context (the context was added only to ease the testing setup), but the problem persisted.
Related to this what I noticed is that when 2 requests are made, the underlying useSuspenseFragment used in the inner components keeps the component in loading state, like if it is trying to read from a different cache (I’m passing the same client from useApolloV2Client). But looking at the Apollo dev tools the cache seems populated.
Is there an easy way to check if the requests are being triggered by different instances? I’m using a link to log the requests, idk if there’s some “client id” i can get from there. Or even the client ref directly.

I understand it’s hard to understand what’s happening, I’ll try to provide a working example to reproduce.

And yes, I’m using Apollo client v4, the v2 refers to the API. Since the API part exposes 2 different schemas on different domains I didn’t find a better way to handle it than using 2 separate clients (some entities share the same name between schemas so sharing the client cache was not feasible).

You can always check operation.client within a link to get the client instance it’s working on.
If you specify a name in the devtoolsOptions, that’s probably also the best way to distinguish the clients when logging.

I’ll try to provide a working example to reproduce.

Please do so! :slight_smile:

I checked a bit and the problem seem to happen because I have two useSuspenseQuery that reference the same entity. Or in other words two components query the same entity requesting different data; each component is wrapped by Suspense.
Here’s a starting point to reproduce GitHub - raxell/apollo-multiple-requests-repro
I just noticed that the mocks in the example lack the __typename, adding it seems to fix the problem, but my real case has both __typename and id in the two queries. I’ll continue to investigate in the following days and I’ll update the example too.

The doubt I have now is: is it possible that Suspense cause some kind of concurrency issues? I don’t know much about the react internals of how it works nor I know where to find those details. All that suspense thing feels a bit flaky on the react side

There are known problems with sibling preloading that apparently haven’t been fully solved yet, most likely caused by [React 19] Prewarm with use() broken in certain state-change situations in the parent · Issue #31566 · facebook/react · GitHub.


In general, what is the situation you are seeing?

  • userInfo1 and userInfo2 queries both running? That would be expected
  • userInfo1 running, userInfo2 running and then userInfo1 running again? That would mean that the response of userInfo2 returns some non-normalizable data overwriting data that’s important for userInfo1 and removing it from the cache, so userInfo1 has to run again.
  • something else?

As a best practice that would (as a side-effect) probably avoid this: we recommend to use fragment colocation and only send out one query per route. Not really for this, but it just generally drops “duplicated data loaded” and eliminate waterfalls. See this video from my teammate Jerel:

Yes absolutely, colocated fragments with data masking is the approach we are moving to. But unfortunately it’s not always applicable (e.g. a fixed sidebar and the main content should fetch data independently; if the content happens to be a page that fetches the same entity that would result in 2 different queries for the same entity, but it’s correct that those remain separated queries).

Regarding the situation (not in the example but in the real app), this is what happens:

  • userInfo1 running, userInfo2 running and then userInfo2 running again

Depending on how what components I have on the page I could see

userInfo1
userInfo2
userInfo2
userInfo1
userInfo2
otherQueryX
otherQueryY
userInfo2

There’s no other component that triggers userInfo2 and the component is rendered a single time (even though rendering multiple times should make no difference given request deduplication).

If I remove the component that triggers userInfo1, then a single request for userInfo2 is made

Could you share these queries? Usually the reason for something like that would be an overlapping sub-query on a non-normalized child, or a missing id or other keyfield somewhere.

Ok yes, it seems to be what you just said.
The 2 queries access a field customer in the following 2 ways:

  customer {
    firstName
  }
  customer {
    id
    firstName
  }

The first has no id because it’s not used, adding it solves the problem.
So, to always be safe the id should always be requested for every entity regardless if it’s used or not? The __typename is always added by the document transform and the id needs to be added manually (I imagine because the name of the id field is not fixed as the __typename)

So, to always be safe the id should always be requested for every entity regardless if it’s used or not?

Yes, always. We can’t add it since we don’t know if your schema has an id field and your server would error if it were not present but we added it.

1 Like

I have another question about this.
It seems that this missing id was also causing a loop of endless requests, resulting in API being rate limited, e.g.

userInfo1
userInfo2
userInfo1
userInfo2
userInfo1
userInfo2
userInfo1
userInfo2
userInfo1
userInfo2

But the loop was only happening to a very very small percentage of users.
Why that? I would expect the behaviour to always be the same (e.g. always loop or never). Is there something that could cause race conditions that could result in this loop?