Improving performance and reducing cost by content caching in iOS Apps
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.
- Add
cache-control
headers to all the images - 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:
- 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. - URLCache
AlamofireImage also utilizesURLCache
, which plays the main role in saving cost. AsURLCache
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:
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.