Deep dive into HTTP caching
As a young mobile apps developer I didn’t care much about practical aspect of HTTP communication. Just added dependency with HTTP client library, made some GETs, POSTs, sometimes PUTs or DELETEs, depending on API specs given by backend team and voila — everything worked. So why should care what this library was doing?
And then — big project, lot of issues in area of communication that was written from scratch and requirement to decrease network traffic. “Let’s do some caching!” — somebody said. “Ok, but how to do this correctly?” — I asked. And that was a good starting point to really understand how caching of network communication works.
So — why do we need cache?
- Decreasing network traffic
- Faster content delivery
- Reduced load of network devices
Who participates in the caching
- Gateway/Shared cache — public cache, common for multiple clients (e.g. image, CSS)
- Private cache — done by client side, can keep sensitive data (e.g. javascript with personal data)
Cache is controlled by HTTP headers
- Pragma
- Vary
- Date
- Expires
- Cache-Control
- ETag
- If-None-Match
- Last-Modified
- If-Modified-Since
How cache works?
Expiration model vs. validation model
- Expiration model — response has certain ‘time’ for which it is still current (‘fresh’). After time elapsed response is treated as ‘stale’
- Cache-Control — this header for example, may contain “max-age” specifying how long the answer is valid
- Expires — this header determines till what time the answer is valid - Validation model — when response is no longer valid (based on expiration model) instead of getting a new one we just check with a server if we have to get new response or can use the old one
- ETag (Entity Tag) & If-None-Match
- Last-Modified & If-Modified-Since
Request cache vs. Response cache
HTTP cache policy can be controlled by both sides of communication. Client app can determine if it wants to accept response from local cache or maybe force to get response from the server. On the other hand server can forbid caching or tells how long client can treat response as valid.
- Request cache — controlling response from client side, e.g. ‘only-if-cached’ (forces to get response only from cache if exists, otherwise do not do a call to server for new response), ‘max-age=0’ (forces validation of cached response with a server)
- Response cache — server tells what can be cached, by who, for how long and in which way, e.g. ‘private’, ‘public’, ‘no-store’, ‘no-cache’
Header: Pragma
- Most known usage is: ‘Pragma: no-cache’
- It is used only because of caompatybility with HTTP 1.0. For every side that uses HTTP 1.1 more important is ‘Cache-Control’ header, by Pragma should still be transferred by all communication nodes, because it is unknown if last node (client) handles HTTP 1.1
- If there is no ‘Cache-Control’ then ‘Pragma’ is handled by HTTP 1.1
- In fact there is no standard for implamantation of this header so we cannot be sure second side will handle it correctly.
Header: Vary
We usually think about cache like a dictionary Uri -> Response, but in fact it’s not true, and can depend on some Request headers. Good example are two clients that accept different types of encoding. Then for each we will have different response for same Uri and both can be cached by server/gateway sides.
Header: Date
- This header tells what time the response has been generated at in RFC 1123 format
- It is required in HTTP Response, but not in Request. For requests it can be added to messages with body like POST, but should not be added, expecially if there is no time synchronization for client.
- In HTTP Response this header may not be added only in certain circumstances like:
- Status code = {100, 101, 5XX}
- Server has no time synchronization (next network node should add this header in such situation) - e.g. ‘Date: Tue, 15 Nov 1994 08:12:31 GMT’
- ‘Date’ header is used to calculate age of response (and compared e.g. to ‘Cache-Control: max-age’ parameter)
Header: Expires
- Most common way of setting response validation time
- Contains time in RFC 1123 format
- If response should be valid only in moment client receive it this header should be set with the sme value as ‘Date’ header.
- Cannot be higher than 1 year ahead
- Parameters ‘max-age’ and ‘s-max-age’ from ‘Cache-Control’ header has hidher importance than ‘Expires’
Header: Cache-Control
This is the most advanced and powerfull header for controlling HTTP caching strategy. It contains instruction about cache policy and can be added by both client (Request cache) and server/gateway (Response cache). Below are listed all possible parameters for both sides.
To understand how these parameters impact cache control we need to analyze all of them. As some are very relevant each other we can group them and describe together. Believe me — it is no so complicated as it can seems to you now.
Let’s start with ‘no-cache’ and ‘no-store’:
‘no-store’
- Forbids anyone anyway cache query/response
- Used for messages with sensitive data
- Response: Indicates that the recipient cannot save message in any persistant storage and should remove it from volatile memory as soon as possible. But there is no guarantee that the recipient will do this!
- Request: Indicates that we don’t accept message from cache
‘no-cache’
- Response: Indicates that response cannot be used in next same query withour checking with server if it is still valid
- Request: Indicates that query must be validated with the server
‘private’
- Message cannot be cached in Shared Cache = can be cached only by the recipient
‘public’
- Message can be cached by anyone (any node in communication)
- Also in case when normally it would not be cached (because of other headers like ‘Authorization’)
Let’s move on to next parameters: ‘max-age’ and ‘s-maxage’ but only for response (‘max-age’ can be used in both request and response):
‘max-age’
- Indicates max time for which response is valid
- Value is seconds from ‘generation’ of message (header ‘Date’)
- It has precedens over header ‘Expires’
‘s-max-age’
- Same as ‘max-age’ but for Shared Cached (private cache ignores this parameter’
- It has precedens over ‘max-age’ and header ‘Expires’
Ok, now we can take a look for same parameter (‘max-age’) but for Request:
‘max-age’
- Indicates that client requires response that is not older than X seconds from its ‘generation’ (header ‘Date’)
- If message in cache is already stale but it’s ‘younger’ than required time then… it cannot be returned.
Take a look at the example below. Even though response is still valid GET done after 300s will take a message from server:
Another example, but this time response became stale in cache so even max age declared by request has not yet elapsed message will be taken from server:
‘max-stale’
- Indicates that client can accept stale message
- Can have value in seconds that says how stale response can be accepted. If there is no value than any stale message can be returned
- Stae message can be returned by Private Cache od Shared Cache
- If stale response is returned it has to have header ‘Warning’ with value ‘110 (Response is stale)’
- If original response has parameter ‘must-revalidate’ that it cannot be returned in above case.
‘min-fresh’
- Indicates that client requires response that will still be valid for X seconds from its ‘generation’ time
Ok, next parameter is ‘only-if-cached’.
‘only-if-cached’
- Indicates that the only accepted response is the one in cache
- Used in cases when we have to have response immediately
- Response from cache is returned only if it is still valid based on other parameters
- If there is nothing in cache or based on other parameters cached response cannot be returned there is an error response with status code 504 (Gteway Timeout)
- Response from Shared Cache can be accepted only in case there is ‘fixed’ connection between client and node with Shared Cache.
‘no-transform’
- Indicates that Shared Cache cannot change response in any way (e.g. convert image format)
- Used for both request (client doesn’t accept changed message) and response (server doesn’t accept changing message it returns)
‘must-revalidate’
- Response from cache can be returned sometimes even it is stale (e.g. with ‘max-stale’ parameter). But server can force at least validation if such message is still valid
- If it is not possible to validate response (e.g. no internet connection) then it cannot be returned (response with ‘504 (Gateway timeout)’ status code)
- ‘no-cache’ means the same as ‘max-age=0, must-revalidate’
- Teoretically, for some User-Agents in some very ‘specific’ scenarios it is still possible to return non-validated message but user need to informed about it and accept such response (based on HTTP standard)
‘proxy-revalidate’
- Same as ‘must-revalidate’ but for Shared Cache
- Private Cache can
Finally we analyzed all parameters of ‘Cache-Control’ header for HTTP standard. It is most powerfull header for controlling cache behavior. Let’s see once again what are all headers used for this mechanism:
- Pragma
- Vary
- Date
- Expires
- Cache-Control
- ETag
- If-None-Match
- Last-Modified
- If-Modified-Since
There are still 4 headers left, but believe me — they are simpler than you think right now after getting familiar with all bunch of possible parameters for Cache-Control.
Response validation models
4 left headers are in fact two pairs of headers used together and each pair is related to one of two validation models:
1. Time-based validation
- In Response server adds header ‘Last-Modified’ with date of resource modification
- In Request client adds header ‘If-Modified-Since’ with the same date (from last response for same query)
- Server verifies if resource has not been modified and response is still valid. If yes then Response has status code ‘304 (Not Modified)’
- If resource has changed Response contains new data
2. Content-based validation
- In Response server adds header ‘ETag’ with unique identifier (e.g. MD5 hash)
- In Request client adds header ‘If-None-Match’ with the unique identifier (from last response for same query)
- Server verifies if resource has not been modified and response is still valid. If yes then Response has status code ‘304 (Not Modified)’
- If resource has changed Response contains new data
As you can see two validation models works in a very smilar way. Which one should you use for your purposes? It depends as always :) With time based validation you don’t have to execute additional operation to generate unique identifier. On the other hand if your data doesn’t change frequently comparing two values of simple data type (e.g. Integer) is much faster than comparison of two Dates. Before you decide it’s always a good idea to consider all pros and cons and analyze these two solutions more deeply.
Cache vs. request type
Ok, we are almost done. Before we end there is one more thing that should be obvious but it’s good to mention. What type of request can be cached.
- GET, HEAD — can be cached (‘idempotent’* request)
- PUT, DELETE — cannot be cached (not ‘idempotent’* request)
- POST — can be (only response) with proper Cache-Control header, but in general it is very bad practice, some internet browsers don’t cache responses for such requests anyway
- ‘idempotent’ — ‘is the property of certain operations in mathematics and computer science whereby they can be applied multiple times without changing the result’ [Wikipedia]
As you can see, caching in HTTP is not so simple and due to it’s development in time there are couple of headers and parameters possible to use. Depending on your solution, available resources and requirements for frontend applications (web or mobile) different strategy can be aplied. You have to decide by yourself, but to not keep it on default settings (probably no cache at all) it’s good to know details and alternative mechanisms. Good luck with your project!