[Client Core Developers] Possible memory leak in ApolloClient QueryManager, despite cache policy set to no-cache?

Hi there!

Looking for some help in resolving a potential memory leak coming from the Apollo Client.

In a production service, we have observed that we are seeing a memory leak. We enabled live profiling of our service using DataDog and can see that, despite our caching policy for query and mutations being set to “no-cache” in the apollo client constructor call, we are still seeing a large leak of memory coming from the QueryManager.getObservableFromLink & Object-Canon.admit method.

I have downloaded the source code for apollo client and taking a look inside the QueryManager.getResultsFromLink() I can’t seem to find where the no-cache policy is truly honored. I do see it doesn’t make an explicit call to writeCache() in the QueryInfo class source code, but I do see a InMemoryCache object being passed around throughout the call stack, and a diff stored in QueryInfo class.

I also want to note we haven’t been able to successfully reproduce the memory leak in a local or stage environment, I think it might have to with throughput, but im not entirely sure.

Anyways, Im hoping someone with deep core knowledge might be able to provide some insight.

My questions would be:

  • Why does ApolloClient require a InMemoryCache instance for a ApolloClient config that sets a no-cache policy?
  • What could possibly be leaking memory, or containing a dangling reference from QueryManager?
  • Any potential debugging tips or solutions?

Why does ApolloClient require a InMemoryCache instance for a ApolloClient config that sets a no-cache policy?

Generally, a cache can also do things like applying custom document transforms, so that cache can have some features that would even apply to queries not stored in the cache.
Apart from that: As Apollo Client is mostly a cache at heart, it is just not an expected use case that you would use it with a blanket “no-cache” fetchPolicy.
Of course, you can do that, but we do not expect a lot of our users to actually do it, and as a result do not ship extra code that would make things work “without a cache”. It will not be written to in your use case, but we do not expect it to be undefined either.

What could possibly be leaking memory, or containing a dangling reference from QueryManager?

From the back of by head: Observables that you subscribed to with client.watchQuery, but never unsubscribed from, promises that are still retrieving data and haven’t finished yet, or simply memory that is not referenced anywhere anymore, but has not been garbage-collected by your engine yet.

The latter might actually be what is happening in your case, since ObjectCanon stores a “canonical” version of every object it encouters in a WeakMap, to make sure that canon.admit({ foo: "bar"}) has the same reference as canon.admit({ foo: "bar" }) besides the fact that these two objects are structurally identical, but not referentially. That WeakMap might just not have been collected by your garbage collection yet, despite there not being a reference to the underlying object anymore.

=> First thing I would try in your case is to trigger manual garbage collection, just to see if memory usage is going down from that. Generally, this “uncollected” garbage should not be a problem, as the engine will collect garbage once it starts hitting memory limits and needs to free up new memory - but of course, depending on your environment, you might make the choice to manually collect garbage from time to time.

Thanks @lenz for the response… Ill post an update here incase it helps. My team was unable to find the root problem, however another team discovered and resolved theirs.

Since we are using apollo client serverside, it definitely had some unintended side effects.

Basically they kept const serverside and clientside clients in code for the next app. Turns out any query made in-process gets added to all clients’ in-flight query registry, but they only get removed when the client resolves the query promise or you explicitly clear the registry. So our “clientside” client had an infinitely growing query registry on the server. Fix was to only create the client within the App component rather than as a module-level const.

Hope this provides some clarity for others. Thanks again for your response!