Ruby on Rails performance is a topic that has been widely discussed. Whichever the conclusion you want to make about all the resources out there, the chances you'll be having to use a cache server in front of your application servers are pretty high. Varnish is a nice option when having to deal with this architecture: it has lots of options and flexibility, and its performance is really good, too.

However, adding a cache server in front of your application can lead to problems when the page you are serving has user dependent content. Let's see what can we do to solve this problem.

The thing about user dependent content is that, well, depends on the user visiting the site. In Rails applications, user authentication is usually done with cookies. This means that as soon as a user has a cookie for our application, the web browser will issue it along the request. Here comes a big problem: Varnish will not cache content when it's requested with a cookie. This will kill your performance for logged in users, as Varnish will simply forward all those requests to the application.

A good approach to get a simple solution to this problem is to add the application cookie to the hash Varnish uses to look for cached content. This is done in the vcl_hash function of the config file:

sub vcl_hash {
   if (req.http.Cookie ~ "your_application_cookie") {
     hash_data(req.url);
     hash_data(req.http.Cookie);
	 return (hash);
   }
}

What we do here is check if the request has a cookie with the name of the cookie we use for authentication. In case this is true, we add this cookie to the Varnish hash. What this will do is keep a different cache fragment for every logged in user that has visited a certain page. Mission accomplished.

The problem with the above solution is that we will be caching a lot of content that is not really user specific, as we cache entire requests, probably entire web pages. We can opt for a more fine grained solution that is not perfect but will probably give us a bit more of performance. We can issue our main pages without the content that depends on the user, leaving those contents for specific URL's that can be loaded via AJAX after the main page has been loaded. If we use this solution, we can cache almost every single page with a single key (hash), and cache only the content that is really different for each user (for example, the typical menu on the top of the page with your username and other user related info).

To make this solution work, we have to tell Varnish to delete all cookies but the ones that are directed to the user related content URL's. This has to be done both ways: from the client to the application and vice versa. To do so, we have to tweak vcl_recv and vcl_fetch.

In vcl_recv we tell Varnish what to do when it receives a request from the client. So here we will delete the cookies from all the requests but the ones that are needed so our Rails application knows who we are:

sub vcl_recv
{
   if (!(req.url ~ "/users/settings") && 
      !(req.url ~ "/user_menu"))
   {
      unset req.http.cookie;
   }
}

In this case we pass the cookies to all the URL's that match two simple regular expressions. This logic can be whatever you like. The cookie magic is done in the instruction unset req.http.cookie.

With this we have half the solution baked. We also need to do the same thing when our application talks to Varnish: delete the cookies from the application response to the client except the ones that actually log the user in (if we didn't do that we could never log in to the application because we could never receive the cookie from the application). This is done in the vcl_fetch section:

sub vcl_fetch
{
   if !(req.url ~ "^/login") && (req.request == "GET") {
      unset beresp.http.set-cookie;
   }
}

What's done here is tell Varnish to delete the cookies coming from the application responses unless the request matches the specified regexp (and is not a GET request).

And that's all we need to have a little more fine grained control on what content is cached depending on the user using Varnish as our cache server.