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.
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.
cache-controlheaders to all the images
- Ensuring that the device respects the
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
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
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:
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.
AlamofireImage also utilizes
URLCache, which plays the main role in saving cost. As
URLCachehas 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.
To ensure that the
URLCache works as expected, we must ensure that
URLSession has proper configurations to ensure caching.
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
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
Operation of Alamofire Image and Cache policy
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
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.
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.
max-age expires the request is made to the server, but as highlighted in image we have two new headers
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
ETag value) to verify if the image has been modified or not.
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.
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
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 firstname.lastname@example.org.
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.