iOS’s Cache system is lying to you

Or in other words – how to implement caching on iOS.

You should always look into implementing some sort of caching mechanism on your servers, so that you’ll avoid wasting the user’s battery, bandwidth and time.

While doing exactly that recently, I was surprised that the caching system on iOS lies about what it actually gets back from the server.
Let’s back up a bit.

In order for the cache to work, you’ll have to instruct your server to send you back the Cache-control header, so that iOS would know to cache things automatically.
That being the case, though, iOS doesn’t automatically send out the If-Modified-Since header on the way back, so you should take care of that, like so:

Here we take the cached response that the server sent, we extract the last-modified value that it returned to us and we set it as the If-Modified-Since value. Now, next time that we call the server we should get 304 (Not Modified), meaning that the data we have is still valid.

All that’s great, but if you don’t set the Cache-Control header correctly, you might come into some problems, here are the possible values:

    private – instructs the cache along the way that the content is meant for a single user and shouldn’t be cached along the way
    public – everyone can cache the content

For some reason when it was private, the NSURL loading system didn’t cache the response for me, but when switching to public – it worked!

That’s great, case closed, let’s go home.

The issue was, though, that when I tried to call the server again – the NSURL loading system didn’t even send out a request to the server to verify if the cache was fresh.
After some rumbling about the internet, it turned out the reason was the max-age of the cache.

max-age lets the caching system know how long the cache would be fresh, so that it won’t bother checking all the time. If it’s missing, a default value is set, which I couldn’t find out the value of anywhere in the HTTP 1.1 specification, so if you happen to know it – drop me a line in the comments!

To fix this I had the server return a value of public, max-age=0 as the Cache-Control header, which would mean that the cache expires instantly. Obviously in your case you could return some value that makes more sense, depending on your data.

Great, let’s test things out!

When sending the first request, I got back the HTTP status 200 as expected and the response is cached.
When sending the second request, though, the NSHTTPURLResponse of the request was again 200. Uh-oh, should’ve been 304.

In order to see exactly what was going on, I fired up my trusty Wireshark (must-have when dealing with network calls) and I sniffed the data coming in and out of my device.
While inspecting the calls I saw that the second request returned 304, instead of 200, which means that the underlying networking code of NSURLConnection automatically detects the 304, gets the previously cached data and let’s you know that you got 200 with the right data.

Thinking about it, it makes a lot of sense, since it’s handling the whole caching thing, so you won’t have to deal with the 304 response in your code.

It also means that you might go crazy wondering why your cache implementation isn’t working, even though it was working all along!

1 Comment

  1. Hopefully you’re only caching publicly available content with “public”, otherwise you (or rather your users) may run into some nasty issues down the road. Swapping private with public is only a solution if the content isn’t something you have to log in to see, because if ISP caching proxies (for example) cache private data, someone else is likely to see it.


Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.