A path of pain with URLCache eviction and subclassing

URLCache class implements the caching of responses to URL load requests, by mapping NSURLRequest objects to CachedURLResponse objects. It provides a composite in-memory and on-disk cache, and lets you manipulate the sizes of both the in-memory and on-disk portions. You can also control the path where cache data is persistently stored.

If you have been working with URL loading on iOS, you are probably familiar with the URLCache class. You can create an instance, define memory & disk capacity limits, set it to your URLSessionConfiguration object and then start creating URLSessions which will use this cache to store entries in memory and on disk. It has been around since iOS 2.0 (way before URLSession existed).

However, what happens when you hit the disk capacity of your URLCache? Apple hasn't documented any specific eviction strategy. I've assumed for a long time that it would be some kind of LRU eviction. However, this doesn't seem to be the case. Based on my own testing on iOS 16, it seems that URLCache evicts all entries when the disk capacity is reached. This seems to happen at response store time, so your app might be running in the foreground while the URLCache is removing its entries.

It seems that URLCache evicts all entries when the disk capacity is reached.

The "always evict all entries" strategy wasn't acceptable in my use case, so I decided to write a replacement for URLCache which would implement its own storage, and would provide the LRU eviction strategy. Apple specifically mentions this use case for subclassing:

The URLCache class is meant to be used as-is, but you can subclass it when you have specific needs. For example, you might want to screen which responses are cached, or reimplement the storage mechanism for security or other reasons.

However, I immediately ran into problems with trying to use my URLCache subclass with URLSession networking. In summary, URL loading was no longer using cached responses to satisfy any of the requests, even though cached responses provided by my URLCache subclass were identical to the responses which were provided by the standard URLCache superclass.

I was sure that I missed something, so I documented my test case and posted a question to Stack Overflow and Apple Developer Forums.

Within less than 24 hours, I received a reply from Quinn "The Eskimo":

My experience is that subclassing Foundation’s URL loading system classes puts you on a path of pain [1].  If I were in your shoes, I’d do your custom caching above the Foundation URL loading system layer.

Share and Enjoy
Quinn “The Eskimo!” @ Developer Technical Support @ Apple

[1] Way back in the day the Foundation URL loading system was implemented in Objective-C and existed within the Foundation framework. In that world, subclasses mostly worked.  Shortly thereafter — and I’m talking before the introduction of NSURLSession here — the core implementation changed languages and moved to CFNetwork.  Since then, the classes you see in Foundation are basically thin wrappers around (private) CFNetwork types.  That’s generally OK, except for the impact on subclassing.

So I decided to write this blog post to bring some visibility to this issue. Hopefully it will save your time, if you are thinking about subclassing URLCache.

If you need to control the eviction strategy, or to implement your own storage, you'll have to do custom caching completely outside Foundation URL loading.