What is the HTTP cache?
Have you ever wondered what happens when you push a “Back” button in your browser? Or when you open a page’s source code to inspect it? More often than not, the browser will load a code that has been “remembered” for later use instead of sending another request to the page’s host server. These are basic examples of using local (private) cache. Some of the HTTP response headers can control if and when the browser should send an actual request for a resource and we can use them to our advantage. Below you can check out the most commonly used ones:
- Cache-control can contain a set of directives describing how the resource should be cached. Possible directives are:
- max-age & s-maxage - the first one tells the browser how long the resource is considered fresh. After the number of seconds specified in this directive, the resource becomes stale. The second one does basically the same thing but is meant for public caches, like proxies - a browser will ignore it completely.
- no-store - the response cannot be cached at all, it needs to be re-downloaded each time.
- public & private - they describe if a resource contains any data that is user-specific and thus shouldn’t be stored in public caches because it won’t benefit other users anyway. Public resources can be put into both public and private caches while private can go only into the latter.
- must-revalidate & no-cache - the first directive tells a browser to revalidate stale (outdated) resources before deciding if it should serve a cached copy. The second one forces revalidation on each request. Revalidation is a process of asking a server if a resource in the cache is still fresh. “But how the server might know that?” - you might ask. “HTTP is a stateless protocol, meaning the server has no idea what response was sent to the browser before!” That’s true and to take care of it, two additional headers have been introduced, described below.
- Last-modified contains the date of the last modification done on the resource. Whenever a browser tries to revalidate this resource, it will send a request with an If-Modified-Since header attached and set its value as a date from the Last-modified header of the previous response. Then the server is able to decide if the resource is still fresh and if that’s the case, the 304 Not Modified response will be sent with no content. Otherwise, a fresh response will be generated and sent with an updated Last-modified header value.
- ETag (entity tag) works similar to Last-modified but instead of modification date, it contains an identifier that is interpreted as a resource’s version. A browser will then include the If-None-Match header into subsequent requests, so the server (or more often - shared cache) can skip response generation and send the 304 Not Modified response if resource versions match. It can be used when the changes to the resource can be done more often than once per second or when the modification dates are not maintained and cannot be easily obtained.
- Vary is a list of HTTP headers that should be checked when deciding if the cached response could be used. Normally, it’s common to have all headers taken into account, but the Vary header can explicitly limit this behavior to a few chosen ones.
What are the benefits of using a proxy cache like Varnish?
Cache-related request headers described above can improve user’s experience whenever they spend some time on a page and are able to actually use the cached resources. However, users hitting your app for the first time or after a break long enough to make their cache outdated will still need to wait for the server to generate content (be it HTML page or JSON API response).
You can establish a Varnish cache server in front of your application. This way, parts of your website can be cached in one place and served to each user in no time! At this point, a bottleneck slowing down your app might be the size of transferred data but Varnish can help you with that as well by compressing cached responses to save some of the precious bandwidth.
What will I need to do to make my site Varnish-friendly?
First of all, you’ll need to ensure that your application includes proper HTTP cache headers in responses. Varnish uses them to determine how long they should be stored in the cache and how to revalidate them when they become stale.
Second, you need to consider what parts of the application can be shared between different users. Very often two users sending slightly different requests receive vastly different responses. For example, the YouTube home page will show a different set of movies based on user’s subscriptions, preferences (represented by likes and comments), a region a request was sent from and many more variables. But some elements of the page remain fairly static, like header and sidebar menu. You can split a page into smaller pieces, that will be cached separately and then glue them back together using Edge Side Includes (ESI) markup language. The simplest include tag looks like that:
<esi:include src="/partials/menu.html"/>
When the Varnish ESI processor encounters such a tag, it will send a request at the URL from the src attribute and replace the tag with a response content. It’s up to you to decide what should be extracted into ESI tags but you should consider that each tag has a chance of triggering an additional request to the backend. Unless you’re requesting static content, overusing it may lead to slowing down your app for as long as the cache is empty, depending on how big overhead of your framework is.
Third, you’ll need to run and configure a Varnish server. While the installation process is quite easy, the configuration proves to be a bit more challenging. By default, only responses for GET and HEAD requests not containing any authorization headers or cookies are cached. The same goes for the response - setting a cookie will prevent any form of caching. Generally, it’s better to not start a session if it’s not necessary. If it happens though, it’s possible to strip the cookie before a hash that identifies a request is generated and also before putting a response in the cache.
Configuring Varnish
Varnish configuration is stored in *.vcl files. VCL stands for Varnish Configuration Language - it’s a domain-specific language loosely based on the C programming language. In fact, Varnish translates these files into the C code and compiles them to shared objects using gcc. This means they’re blazing fast and if you already know C or a similar language, you should have no problem learning VCL.
VCL utilizes a concept of a finite state machine. States are represented by different built-in subroutines (exclusively prefixed with vcl_) and transitions between these states are determined by an action keyword returned by a subroutine. For example, let’s analyze the default vcl_recv subroutine which is responsible for handling incoming requests:
/* sub stands for "subroutine". A new subroutine can be defined if
necessary but keep in mind that it’s not a function equivalent
and thus, it’s quite limited - it cannot take any arguments and has
to return an action. */
sub vcl_recv {
/* The req variable contains data from a client’s request. */
if (req.http.host) {
/* Normalize hostname - convert all characters to lowercase.
*/
set req.http.host = req.http.host.lower();
}
if (req.method == "PRI") {
/* SPDY or HTTP/2.0 protocols are not supported - return
an error. */
return (synth(405));
}
if (!req.http.host &&
req.esi_level == 0 &&
/* Tilde operator (~) is used to evaluate regular expressions. */
req.proto ~ "^(?i)HTTP/1.1") {
/* In HTTP/1.1, Host is required - return error if not set. */
return (synth(400));
}
if (req.method != "GET" &&
req.method != "HEAD" &&
req.method != "PUT" &&
req.method != "POST" &&
req.method != "TRACE" &&
req.method != "OPTIONS" &&
req.method != "DELETE") {
/* In case of an unknown request method, pass the request
directly to the backend server. The response
will not be modified nor stored in the cache. */
return (pipe);
} if (req.method != "GET" && req.method != "HEAD") {
/* If the request method is not GET or HEAD, pass the request
to the backend server. The response can still be modified
before sending it back to the client and possibly cached
but the latter would be pointless since there is no way of
utilizing the cached value in this script. */
return (pass);
}
if (req.http.Authorization || req.http.Cookie) {
/* Don't cache the response for (possibly) authenticated
users. It would allow hijacking someone’s session.
Just pass it to the backend server. */
return (pass);
}
/* Otherwise, check if there’s a cached response. */
return (hash);
}
For good measure, we’ll also take a look at vcl_backend_response subroutine that takes care of formatting a backend response before sending it to the client and possibly storing it in the cache.
sub vcl_backend_response {
/* bereq stands for backend request and beresp stands for backend
response. */
if (bereq.uncacheable) {
/* Pass the response to the client if it shouldn’t be cached.
*/
return (deliver);
} else if (beresp.ttl <= 0s ||
/* ^ Note that time units are used to define a duration. */
beresp.http.Set-Cookie ||
beresp.http.Surrogate-control ~ "no-store" ||
(!beresp.http.Surrogate-Control &&
beresp.http.Cache-Control ~ "no-cache|no-store|private") ||
beresp.http.Vary == "*") {
/* If response’s calculated TTL (time to live) is less than or
equal to 0; or response contains a cookie; or backend
explicitly requested to not store the response, then mark
it as uncacheable for two minutes. It means that for next
two minutes Varnish will "remember" to pass all requests
for that particular URL to the backend server. */
set beresp.ttl = 120s;
set beresp.uncacheable = true;
}
/* Otherwise, the response is stored in the cache normally and
sent to the client. */
return (deliver);
}
These built-in subroutines are a good entry point for creating your own configuration files. You can check other built-in subroutines on the GitHub repository.
How can you benefit from Varnish?
Varnish is quite easy to use and the results can be surprisingly good. However, a backend developer has to put some work into the application for Varnish to work as intended. It may appear daunting to implement cache header handlers but the payoff is definitely worth it - I’ve seen applications loading up to ten times faster without any refactoring (and I’m talking about production server located thousands of kilometers away; on a local machine the results were ten times better). It doesn’t matter if you’re a PHP developer, a Python developer or if you’re working with any other HTTP-based technology, you can still benefit from all the advantages Varnish gives you and mastering it can prove to be a valuable skill.
Navigate the changing IT landscape
Some highlighted content that we want to draw attention to to link to our other resources. It usually contains a link .