HTTP Cache

HTTP Cache

The nature of rich web applications means that they're dynamic. No matter how efficient your application, each request will always contain more overhead than serving a static file. Usually, that's fine. But when you need your requests to be lightning fast, you need HTTP caching.

Caching on the Shoulders of Giants

With HTTP Caching, you cache the full output of a page (i.e. the response) and bypass your application entirely on subsequent requests. Of course, caching entire responses isn't always possible for highly dynamic sites, or is it? With Edge Side Includes (ESI), you can use the power of HTTP caching on only fragments of your site.

The Symfony cache system is different because it relies on the simplicity and power of the HTTP cache as defined in the HTTP specification. Instead of reinventing a caching methodology, Symfony embraces the standard that defines basic communication on the Web. Once you understand the fundamental HTTP validation and expiration caching models, you'll be ready to master the Symfony cache system.

Since caching with HTTP isn't unique to Symfony, many articles already exist on the topic. If you're new to HTTP caching, Ryan Tomayko's article Things Caches Do is highly recommended . Another in-depth resource is Mark Nottingham's Cache Tutorial.

Caching with a Gateway Cache

When caching with HTTP, the cache is separated from your application entirely and sits between your application and the client making the request.

The job of the cache is to accept requests from the client and pass them back to your application. The cache will also receive responses back from your application and forward them on to the client. The cache is the "middle-man" of the request-response communication between the client and your application.

Along the way, the cache will store each response that is deemed "cacheable" (See Making your Responses HTTP Cacheable). If the same resource is requested again, the cache sends the cached response to the client, ignoring your application entirely.

This type of cache is known as a HTTP gateway cache and many exist such as Varnish, Squid in reverse proxy mode, and the Symfony reverse proxy.

Tip

Gateway caches are sometimes referred to as reverse proxy caches, surrogate caches, or even HTTP accelerators.

Symfony Reverse Proxy

Symfony comes with a reverse proxy (i.e. gateway cache) written in PHP. It's not a fully-featured reverse proxy cache like Varnish, but is a great way to start.

Tip

For details on setting up Varnish, see How to Use Varnish to Speed up my Website.

Enabling the proxy is easy: each application comes with a caching kernel (AppCache) that wraps the default one (AppKernel). The caching Kernel is the reverse proxy.

To enable caching, modify the code of your front controller. You can also make these changes to app_dev.php to add caching to the dev environment:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// web/app.php
use Symfony\Component\HttpFoundation\Request;

// ...
$kernel = new AppKernel('prod', false);
$kernel->loadClassCache();

// add (or uncomment) this new line!
// wrap the default AppKernel with the AppCache one
$kernel = new AppCache($kernel);

$request = Request::createFromGlobals();
// ...

The caching kernel will immediately act as a reverse proxy: caching responses from your application and returning them to the client.

Caution

If you're using the framework.http_method_override option to read the HTTP method from a _method parameter, see the above link for a tweak you need to make.

Tip

The cache kernel has a special getLog() method that returns a string representation of what happened in the cache layer. In the development environment, use it to debug and validate your cache strategy:

error_log($kernel->getLog());

The AppCache object has a sensible default configuration, but it can be finely tuned via a set of options you can set by overriding the getOptions() method:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// app/AppCache.php
use Symfony\Bundle\FrameworkBundle\HttpCache\HttpCache;

class AppCache extends HttpCache
{
    protected function getOptions()
    {
        return array(
            'default_ttl' => 0,
            // ...
        );
    }
}

For a full list of the options and their meaning, see the HttpCache::__construct() documentation.

When you're in debug mode (either because your booting a debug kernel, like in app_dev.php or you manually set the debug option to true), Symfony automatically adds an X-Symfony-Cache header to the response. Use this to get information about cache hits and misses.

The Symfony reverse proxy is a great tool to use when developing your website or when you deploy your website to a shared host where you cannot install anything beyond PHP code. But being written in PHP, it cannot be as fast as a proxy written in C.

Fortunately, since all reverse proxies are effectively the same, you should be able to switch to something more robust - like Varnish - without any problems. See How to use Varnish

Making your Responses HTTP Cacheable

Once you've added a reverse proxy cache (e.g. like the Symfony reverse proxy or Varnish), you're ready to cache your responses. To do that, you need to communicate to your cache which responses are cacheable and for how long. This is done by setting HTTP cache headers on the response.

HTTP specifies four response cache headers that you can set to enable caching:

  • Cache-Control
  • Expires
  • ETag
  • Last-Modified

These four headers are used to help cache your responses via two different models:

  1. Expiration Caching Used to cache your entire response for a specific amount of time (e.g. 24 hours). Simple, but cache invalidation is more difficult.
  2. Validation Caching More complex: used to cache your response, but allows you to dynamically invalidate it as soon as your content changes.

All of the HTTP headers you'll read about are not invented by Symfony! They're part of an HTTP specification that's used by sites all over the web. To dig deeper into HTTP Caching, check out the original RFC 2616 document, or these two other pieces: P4 - Conditional Requests and P6 - Caching: Browser and intermediary caches.

As a web developer, you are strongly urged to read the specification. Its clarity and power - even more than fifteen years after its creation - is invaluable. Don't be put-off by the appearance of the spec - its contents are much more beautiful than its cover!

Expiration Caching

The easiest way to cache a response is by caching it for a specific amount of time:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// src/AppBundle/Controller/BlogController.php
use Symfony\Component\HttpFoundation\Response;
// ...

public function indexAction()
{
    // somehow create a Response object, like by rendering a template
    $response = $this->render('blog/index.html.twig', []);

    // cache for 3600 seconds
    $response->setSharedMaxAge(3600);

    // (optional) set a custom Cache-Control directive
    $response->headers->addCacheControlDirective('must-revalidate', true);

    return $response;
}

Thanks to this new code, your HTTP response will have the following header:

1
Cache-Control: public, s-maxage=3600, must-revalidate

This tells your HTTP reverse proxy to cache this response for 3600 seconds. If anyone requests this URL again before 3600 seconds, your application won't be hit at all. If you're using the Symfony reverse proxy, look at the X-Symfony-Cache header for debugging information about cache hits and misses.

Tip

The URI of the request is used as the cache key (unless you vary).

This is super performant and simple to use. But, cache invalidation is not supported. If your content change, you'll need to wait until your cache expires for the page to update.

Tip

Actually, you can manually invalidate your cache, but it's not part of the HTTP Caching spec. See Cache Invalidation.

If you need to set cache headers for many different controller actions, check out FOSHttpCacheBundle. It provides a way to define cache headers based on the URL pattern and other request properties.

Finally, for more information about expiration caching, see HTTP Cache Expiration.

Validation Caching

With expiration caching, you simply say "cache for 3600 seconds!". But, when someone updates cached content, you won't see that content on your site until the cache expires.

If you need to see updated content immediately, you either need to invalidate your cache or use the validation caching model.

For details, see HTTP Cache Validation.

Safe Methods: Only caching GET or HEAD requests

HTTP caching only works for "safe" HTTP methods (like GET and HEAD). This means two things:

  • Don't try to cache PUT, POST or DELETE requests. It won't work and with good reason. These methods are meant to be used when mutating the state of your application (e.g. deleting a blog post). Caching them would prevent certain requests from hitting and mutating your application.
  • You should never change the state of your application (e.g. update a blog post) when responding to a GET or HEAD request. If those requests are cached, future requests may not actually hit your server.

More Response Methods

The Response class provides many more methods related to the cache. Here are the most useful ones:

1
2
3
4
5
// Marks the Response stale
$response->expire();

// Force the response to return a proper 304 response with no content
$response->setNotModified();

Additionally, most cache-related HTTP headers can be set via the single setCache() method:

1
2
3
4
5
6
7
8
9
// Set cache settings in one call
$response->setCache(array(
    'etag'          => $etag,
    'last_modified' => $date,
    'max_age'       => 10,
    's_maxage'      => 10,
    'public'        => true,
    // 'private'    => true,
));

Cache Invalidation

Cache invalidation is not part of the HTTP specification. Still, it can be really useful to delete various HTTP cache entries as soon as some content on your site is updated.

For details, see Cache Invalidation.

Using Edge Side Includes

When pages contain dynamic parts, you may not be able to cache entire pages, but only parts of it. Read Working with Edge Side Includes to find out how to configure different cache strategies for specific parts of your page.

Summary

Symfony was designed to follow the proven rules of the road: HTTP. Caching is no exception. Mastering the Symfony cache system means becoming familiar with the HTTP cache models and using them effectively. This means that, instead of relying only on Symfony documentation and code examples, you have access to a world of knowledge related to HTTP caching and gateway caches such as Varnish.

This work, including the code samples, is licensed under a Creative Commons BY-SA 3.0 license.