Boost WordPress performance with Varnish Cache

Lets face it, WordPress is slow. With every request it has to go though thousands lines of code and multiple SQL queries to render a page. Very popular configuration for a WordPress site is Apache, mod_rewrite, mod_php, PHP and MySQL. It’s very good setup but can’t be consider the fastest (at least without any additional tweaking).
The good news is WordPress doesn’t have to be a speed demon. In most cases it’s just a CMS to produce static pages. If the content is static it doesn’t make any sense to waste CPU cycles on re-rendering the same HTML over and over again.

Currently I’m running this blog on Amazon EC2 Micro instance. It’s the smallest (and slowest) setup you can get from Amazon. The micro instance has 613 MB of RAM and a limited access to CPU. When I tried to benchmark it with the Apache Benchamark I got about 1 req/sec… and crashed database. Ten concurrent connections were enough to DoS my blog! Fifteen minutes later which was required to setup Varnish Cache the same hardware could handle 665 req/sec. During the test CPU got to 65% which means I reached broadband limits.

$ ab -c300 -n500

Server Software:        Apache/2.2.22
Server Hostname:
Server Port:            80

Document Path:          /
Document Length:        128174 bytes

Concurrency Level:      300
Time taken for tests:   0.751 seconds
Complete requests:      500
Failed requests:        0
Write errors:           0
Total transferred:      64310000 bytes
HTML transferred:       64087000 bytes
Requests per second:    665.85 [#/sec] (mean)
Time per request:       450.555 [ms] (mean)
Time per request:       1.502 [ms] (mean, across all concurrent requests)
Transfer rate:          83633.83 [Kbytes/sec] received

So why to make a website fast? There are at least 3 reasons:
– It will survive sudden traffic spikes (so called Slashdot effect)
– better Google ranking
– according to Google’s research there is a correlation between website’s response time and consumed content. In other words the faster website is the higher chance for another click.

Varnish Cache is a web application accelerator also known as a caching HTTP reverse proxy. You install it in front of any server that speaks HTTP and configure it to cache the contents. Varnish Cache is really, really fast. It typically speeds up delivery with a factor of 300 – 1000x, depending on your architecture.

Varnish stands in front of a web server which means it will have to listen on port 80. By default this port it already taken by Apache. The first thing is to change configuration of your web server.

$ vim /etc/apache2/ports.conf

Change port 80 to 8080.

NameVirtualHost *:8080
Listen 8080

    Listen 443

    Listen 443

Edit virtual host settings.

$ vim /etc/apache2/sites-available/architect

Alter port from 80 to 8080.

Now install varnish. Depends on your distribution you might get different version of the software. This article is created for varnish 3.x.

$ apt-get install varnish
$ varnishd -V
varnishd (varnish-3.0.2 revision cbf1284)
Copyright (c) 2006 Verdens Gang AS
Copyright (c) 2006-2011 Varnish Software AS

Edit varnish settings and set port and cache size.

$ vim /etc/default/varnish

Memory on my server is limited so I use only 64 MB. Default value is 254 but quoter of that is enough for a small blog.

DAEMON_OPTS="-a :80 
                              -T localhost:6082 
                              -f /etc/varnish/default.vcl 
                              -S /etc/varnish/secret 
                              -s malloc,64m"

Edit varnish configuration.

$ vim /etc/varnish/default.vcl

Change the VCL script to

backend default {
    .host = "";
    .port = "8080";

sub vcl_recv {
    if (req.restarts == 0) {
        if (req.http.x-forwarded-for) {
            set req.http.X-Forwarded-For =
            req.http.X-Forwarded-For + ", " + client.ip;
        } else {
            set req.http.X-Forwarded-For = client.ip;
    if (req.request == "PURGE") {
        if ( client.ip != "") {
            error 405 "Not allowed.";
        return (lookup);
    if (req.request != "GET" &&
        req.request != "HEAD" &&
        req.request != "PUT" && 
        req.request != "POST" &&
        req.request != "TRACE" &&
        req.request != "OPTIONS" &&
        req.request != "DELETE") {
            return (pipe);
    if (req.request != "GET" && req.request != "HEAD") {
        return (pass);
    if (!(req.url ~ "wp-(login|admin)") &&
        !(req.url ~ "&preview=true" ) ) {
        unset req.http.cookie;

    if (req.http.Authorization || req.http.Cookie) {
        return (pass);
    return (lookup);

sub vcl_hit {
    if (req.request == "PURGE") {
        error 200 "Purged.";
    return (deliver);

sub vcl_miss {
    if (req.request == "PURGE") {
        error 200 "Purged.";
    return (fetch);

sub vcl_fetch {
    if (!(req.url ~ "wp-(login|admin)")) {
        unset beresp.http.set-cookie;
        set beresp.ttl = 96h;

    if (beresp.ttl <= 0s ||
        beresp.http.Set-Cookie ||
        beresp.http.Vary == "*") {
            set beresp.ttl = 120 s;
            return (hit_for_pass);
    return (deliver);

You will have to change the IP to your server’s address.
Everything is in place now so the last thing to do is restartng services.

$ sudo /etc/init.d/apache2 restart
$ sudo /etc/init.d/varnish restart

To make sure caching is working run tail against all apache logs and refresh your website few times.

$ tail -f /var/log/apache2/*

First request will hit Apache server and you should see new entries in the log. Varnish will put all resources for that URL in cache. Every following request shouldn’t populate any new logs.

The VCL configuration makes Varnish cache every GET request for 96 hours. There is an exception for wp-login and wp-admin. Those are dynamic pages and it’s better not to cache them. Your visitors have no reason to go there anyway.

Varnish will cache everything for 96h. If you create a new post or edit an existing one your changes won’t be visible. Varnish doesn’t know something has changed and will continue serving stale content. You can restart the service to wipe all data.

$ /etc/init.d/varnish restart

It’s not very user friendly approach. It also clears more then it should. Don’t worry, it’s WordPress. There is plugin for everything. Install “Varnish HTTP Purge” extension. No configuration is required. Default.vcl file already handlers PURGE requests. Now you can visit your blog, open network tab in Firebug and enjoy response time below 200ms.

Varnish is an amazing peace of software with very powerful features. It can act as a load balancer, it can pull different parts of your website from different palaces (ESI) and if it can’t do something there might be an extension for that. There is nothing to wait for. Download it. Use it.

3 thoughts on “Boost WordPress performance with Varnish Cache

  1. Hello friend,

    When I try to start varnish with this VCL I get the following error:

    Message from VCC-compiler:
    Expected variable, string or semicolon
    (input Line 10 Pos 38)
    req.http.X-Forwarded-For + “, ” + client.ip;
    Running VCC-compiler failed, exit 1
    VCL compilation failed

    What’s happening?


  2. Great stuff — thanks so much for this, it saved me having to figure out the cookie faff to make WordPress admin happy.

    I added one more test to bypass varnish for the Jetpack site monitor, so that it actually tests the back end:

    if (req.http.User-Agent ~ “^jetmon/”) {
    /* Don’t cache so that jetpack can monitor backend health */
    return (pass);


  3. Hi Lucasz,

    Thanks for the configs. I am new to Varnish and am trying to configure it on a test site. I can see that the Varnish headers are being added, but when I do “varnishstat”, I get the below output. Hit ratio and hitrate avg is always 0. Can you point me to the right direction what the below output means?

    Hitrate ratio:        0        0        0
    Hitrate avg:     0.0000   0.0000   0.0000
              41         0.00          inf backend_busy - Backend conn. too many
              67         0.00          inf backend_reuse - Backend conn. reuses
              10         0.00          inf backend_toolate - Backend conn. was closed
              17         0.00          inf backend_retry - Backend conn. retry
              15         0.00          inf fetch_head - Fetch head
              42         0.00          inf fetch_bad - Fetch had bad headers
               9         0.00          inf fetch_close - Fetch wanted close
              54         0.00          inf fetch_oldhttp - Fetch pre HTTP/1.1 closed
               3         0.00          inf fetch_1xx - Fetch no body (1xx)
              51         0.00          inf fetch_204 - Fetch no body (204)
              15          .            .   n_wrk - N worker threads
              16         0.00          inf n_wrk_failed - N worker threads not created
              21         0.00          inf n_wrk_lqueue - work request queue length
              22         0.00          inf n_wrk_queued - N queued work requests
               6         0.00          inf n_wrk_drop - N dropped work requests
               3          .            .   n_backend - N backends
              50          .            .   n_expired - N expired objects
              50          .            .   n_lru_nuked - N LRU nuked objects
               1         0.00          inf s_sess - Total Sessions
               1         0.00          inf s_req - Total Requests
              10         0.00          inf s_pass - Total pass
              58         0.00          inf s_bodybytes - Total body bytes
              41         0.00          inf sess_pipeline - Session Pipeline
              67         0.00          inf sess_readahead - Session Read Ahead
               3         0.00          inf sess_linger - Session Linger
              37         0.00          inf sess_herd - Session herd
              54         0.00          inf shm_records - SHM records
           39194         0.00          inf shm_writes - SHM writes
          868727         0.00          inf shm_flushes - SHM flushes due to overflow
               4         0.00          inf shm_cont - SHM MTX contention
              64          .            .   sms_nobj - SMS outstanding allocations
              63          .            .   sms_nbytes - SMS outstanding bytes
            6797          .            .   sms_balloc - SMS bytes allocated
            1111          .            .   sms_bfree - SMS bytes freed
              54         0.00          inf n_ban_re_test - N regexps tested against
               1         0.00          inf n_ban_dups - N duplicate bans removed
               1         0.00          inf hcb_nolock - HCB Lookups without lock
               1         0.00          inf hcb_insert - HCB Inserts
               1         0.00          inf esi_errors - ESI parse errors (unlock)
               1         0.00          inf esi_warnings - ESI parse warnings (unlock)
              27         0.00          inf dir_dns_failed - DNS director failed lookups
              17         0.00          inf dir_dns_hit - DNS director cached lookups hit
              17         0.00          inf dir_dns_cache_full - DNS director full dnscache



Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s