Apollo iOS bypassing URLSession's cache-control behaviour by checking its own store first in `.returnCacheDataElseFetch`

Hi team,

While digging into caching behavior in Apollo iOS, I noticed something I wanted to clarify. In CacheReadInterceptor, the .returnCacheDataElseFetch currently checks Apollo’s store first:

case .returnCacheDataElseFetch:
  self.fetchFromCache(for: request, chain: chain) { cacheFetchResult in
  switch cacheFetchResult {
  case .failure:
    // Cache miss → proceed to network
    chain.proceedAsync(…)
  case .success(let graphQLResult):
    // Cache hit → return from ApolloStore
    chain.returnValueAsync(…)
  }
}

This seems to bypass URLSession’s built-in caching behavior. Since Apollo iOS doesn’t have TTL or consider Cache-Control: max-age, once data is cached it will never refresh. By contrast, if we simply proceed without checking Apollo’s store first:

case .returnCacheDataElseFetch:
  // Let URLSession + URLCache decide freshness
  chain.proceedAsync(...)

then URLSession and URLCache correctly handles max-age and decide whether to serve from cache or hit the network.

I verified this by checking URLSessionTaskMetrics, in URLSessionClient’s func urlSession(_ session: URLSession, task: URLSessionTask, didFinishCollecting metrics: URLSessionTaskMetrics) which shows whether the response came from localCache or networkLoad.

Looking into the code in v2.0, I’m assuming this is the same behaviour:

public enum CachePolicy_v1: Sendable, Hashable {
  func toFetchBehavior() -> FetchBehavior {
    switch self {
    case .returnCacheDataElseFetch:
      return FetchBehavior.CacheFirst
      ...
    }
  }
extension FetchBehavior {
  public static let CacheFirst = FetchBehavior(
    cacheRead: .beforeNetworkFetch,
    networkFetch: .onCacheMiss
  )
}

My question: is this design choice intentional? Should Apollo always prioritise its own store here, or would it make sense to let URLSession handle freshness instead of Apollo’s store?

Thanks in advance for any clarification! :folded_hands:

1 Like

Hey @grse so the short answer is that the current ApolloStore wasn’t designed to work with URLSession cache information. A big reason for not letting the URLSession cache handle things is because it’s possible to update the normalized cache in the ApolloStore through other means such as local cache mutations which the URLSession wouldn’t know about. Going into 2.0 that functionality will remain the same.

However, we have planned out a significant rework to our caching system that we will begin working on in the near future, which among other things will contain ways to configure TTL for the ApolloStore which I imagine alleviates this issue. There is an RFC up in our GitHub that details the current path forward for the caching rework, and it will likely be updated more as we begin that work in earnest if you want to follow along there: RFC: Caching Rework · Issue #3529 · apollographql/apollo-ios · GitHub

Thank you for the detailed explanation @ZachF-Apollo — that makes sense. I can see local changes to the cache getting overridden if operations are allowed to pass through to URLSession without a check and it returns cached data.

That said, in our use case we’d really like to take advantage of HTTP caching semantics and let URLSession/URLCache decide freshness for most operations as we do not alter the local cache in ApolloStore. Essentially, we’d want a fetch behaviour that skips the store read and just proceeds to the network client.

Are there any recommended approaches to achieve this today? For example:

  • Could this be done cleanly by providing a custom cache implementation that short-circuits the store read?
  • Or would the team be open to an MR that adds a separate fetch behaviour, e.g. CachePolicy.httpCacheFirst, which simply forwards the operation directly to URLSession and then merges the result into ApolloStore?

Either way, we’d much rather align with the direction the team is taking.