Improving performance and reducing cost by content caching in iOS Apps

iOS Mar 15, 2020

At Halodoc, as a startup we are really ambitious towards our goals and are always looking for ways to simplify health care. But while doing that we must also optimize our system for cost and efficiency so that we can consistently provide our services to people.

And as any company that provides its services through the internet, one of the main costs we incur is through the requests made to CDN, i.e Cloudfront in our case. So, lets say cloudfront charges  $0.60 per 1 million requests, and the clients makes around 100k million (based on number of users)requests per month, then it would cost around

(0.60 / 10^6) * (100000 * 10^6) = 60000 dollars per month.

Which is a pretty huge number. So how can we optimize this cost? One of the ways is caching the response for the requests on the client, so that the client doesn’t have to make a request to the server.

But as all responses cannot be cached, so what should be considered for caching? Static files are best for caching, in our case we considered images. So, in this blog we are going to talk about how we made our system cost-aware, by appropriately configuring Cloudfront and client side caching.

Caching in iOS Apps

Most of the apps these days, have a lot of images which are downloaded via server. So every time a new screen is initialized, requests are made to the server which incur cost.

But if you might have already noticed the images are cached by default too, which is correct but without the cache-control header the app will still make the request to verify the freshness of the image,  for this the server will respond with 304 Not Modified. So the image won’t be downloaded again but because the request was still made to the server, you will still be charged.

So, having cache-control headers will not only help reduce the requests to the servers but will also help in improving the performance of the application, as the app can directly use the cached images without having to make any request to the server and wait for the response.

Using Cache-control headers

In order for your images to be properly cached in your users devices, there are two things that need to be ensured.

  1. Add cache-control headers to all the images
  2. Ensuring that the device respects the cache-control headers.

Adding Cache-control headers to the images

So for apps to cache the images we need to set the Cache-control max-age property in Amazon S3. max-age directive specifies the maximum amount of time the image is considered fresh. It is relative to the time when the request was made. Apps and browsers utilize this value to determine whether to make request to the server or not.

Ensuring that the device respects the cache-control headers

Once we have cache-control headers set for the images, we need to ensure that the images are actually being cached and the app is not making any requests to the server.

To begin, first we have to look into how images are actually being cached and what caching mechanism is being used. We use AlamofireImage for the purpose of downloading images. So let’s look into how Alamofireimage works, and how we can ensure that it is caching all the images based on the cache-control headers.

AlamofireImage and Caching

AlamofireImage is a library that works along with Alamofire for handling images. When it comes to image caching AlamofireImage utilizes two caches:

  1. AutoPurgingCache
    This cache is basically a in-memory cache that caches the images until the app is in memory and is cleared once the app is killed. The purpose of this cache is to store images with different filters applied on them so the same operation doesn’t have to be performed again. As it is in memory it only works for a single app lifecycle and does not contribute much to saving cost.
  2. URLCache
    AlamofireImage also utilizes URLCache, which plays the main role in saving cost. As URLCache has both disk cache as well as in memory cache.

How AlamofireImage works with URLCache

Once you ask AlamofireImage for an image, first of all it checks it’s in-memory cache for the image. So on fresh launch the app will never have the image in it’s in-memory cache, in this scenario it will make a request for the image using ImageDownloader. Once the request is made if the response was cached earlier by URLCache and it has cache-control headers which are still valid, the cached response for the image will be returned, otherwise it makes a request to the cloudfront for the image and then the response is cached by URLCache as well as the alamofire autopurging in-memory cache.


AlamofireImage URLSessionConfiguration

To ensure that the URLCache works as expected, we must ensure that URLSession has proper configurations to ensure caching.

AlamofireImage utilizes Alamofire for the purpose of downloading an image from a particular URL. In order to do that it creates a Session(Alamofire Session) with a URLSessionConfiguration. By default Alamofire sets the requestCachePolicy to useProtocolCachePolicy which follows the http protocols. So if you are using the default image downloader of Alamofire you are well and good as it is already following the http protocol. But even if you don’t use AlamofireImage in your project and directly use URLSession, the session configuration should follow the useProtocolCachePolicy so the http headers are respected. If no configuration has been provided for the URLSession it is fine too as by default requestCachePolicy is set to useProtocolCachePolicy.

Operation of Alamofire Image and Cache policy

After the cache-control headers are set, we also wanted to verify that the image was actually being cached and used without any new requests being made to the server, because as mentioned earlier if the cache headers are not set properly URLSession will still make the request to verify the freshness of the image. So, in order to verify that no new requests are being made to the server, we used a proxy to  intercept all the requests and responses.

The image we used had the following cache-control headers:

Cache-Control: public, max-age:7200

Below are request and response for the initial request, while cache-control is still valid and request after the expiration of max-age.

1. Initial Request - Image not in cache
When a initial request for the image is made, we got the following request and response headers with the default `URLCache` configuration of AlamofireImage.

Request:

Response:

In the image received you can observe the image received had an ETag which is used to verify the image whether the content has been modified on the server.

2.Requests while cache-control: max-age header is still valid
It was observed that post the initial request, for the period of 2 hours(7200 seconds) no request were made to the server.

3. Request after cache-control: max-age expires
Below is the request and response of the image whose cache-control max-age header has expired but the image is still in cache.

Request

Once the max-age expires the request is made to the server, but as highlighted in image we have two new headers If-None-Match and If-Modified-Since. The value of if-none-match is the ETag value we received in the response of the initial request and the value of if-modified-since is the value received in the initial response in the Last-Modified key. So as per the documentation of if-none-match it takes precedence over if-modified-since if both are used in combination. Once this request is sent to server it uses the value of if-none-match(ETag value) to verify if the image has been modified or not.

Response:

As specified in the if-none-match documentation the status code received was 304 not modified as the ETag value was found on the server.  So, the device used the same image present in cache.

Conclusion

So based on the number of users for your apps, you can reduce the cost of cloudfront by a huge amount, by reducing the number of requests through cache-control headers.

If you consider the example at the beginning, if all of them were image requests and out of them only 10000 million (based on number of users)were unique, then using proper cache control you would only incur

(0.60 / 10^6) * (10000 * 10^6) = 6000 dollars per month.

which is a huge saving. Its also important to note that images are cached by default but cache-control headers helps the app to inform about the response's freshness so that the app doesn't end up revalidating the image from server.

We always aspire towards exploring new technology and finding new and better solutions to problems by constantly improving upon the code we write and features we release. If you have similar mindset consider reaching out to us with your resumé at careers.india@halodoc.com.

About Halodoc

Halodoc is the number 1 all around Healthcare application in Indonesia. Our mission is to simplify and bring quality healthcare across Indonesia, from Sabang to Merauke. We connect 20,000+ doctors with patients in need through our teleconsultation service, we partner with 1500+ pharmacies in 50 cities to bring medicine to your doorstep, we partner with Indonesia's largest lab provider to provide lab home services, and to top it off we have recently launched a premium appointment service that partners with 500+ hospitals that allows patients to book a doctor appointment inside our application.We are extremely fortunate to be trusted by our investors, such as the Bill & Melinda Gates foundation, Singtel, UOB Ventures, Allianz, Gojek and many more. We recently closed our Series B round and In total have raised USD$100million for our mission.Our team work tirelessly to make sure that we create the best healthcare solution personalized for all of our patient's needs, and are continuously on a path to simplify healthcare for Indonesia.