PHP / Wordpress

PHP and HTTP caching

by , , revisited on

We have by far the largest RPM repository with NGINX module packages and VMODs for Varnish. If you want to install NGINX, Varnish, and lots of useful performance/security software with smooth yum upgrades for production use, this is the repository for you.
Active subscription is required.

I figure there isn’t much all-in-one information on the subject and this will be a constant draft with my findings.

PHP 7 and caching headers

PHP itself alters Cache-Control headers only when all conditions are true at the same time during request:

  • session_start() has been called
  • session.cache_limiter has default value of nocache

It adds 3 caching related headers:

Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate
Pragma: no-cache

In short, the default behaviour is to send anti-caching headers any time sessions are in use, not only when Set-Cookie is being sent for the first time. Anytime!

When session_start() is not leveraged, PHP does not touch Cache-Control and friends at all.

The possible values for session.cache_limiter and session_cache_limiter() are:

none: no header will be sent


Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0
Pragma: no-cache


Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: private, max-age=10800, pre-check=10800


Cache-Control: private, max-age=10800, pre-check=10800


Expires: pageload + 3 hours
Cache-Control: public, max-age=10800

WordPress Cache Headers

WordPress does not send caching headers except for a few specific areas where caching has to be disabled.
Those areas include:

  • Error pages (called via wp_die)
  • A page delivering the fact that database connection could not be established
  • Response to POST-ing a comment
  • When user is is logged in
  • 404 pages
  • etc.

In all those cases, the following anti-caching headers are sent:

'Expires' => 'Wed, 11 Jan 1984 05:00:00 GMT',
'Cache-Control' => 'no-cache, must-revalidate, max-age=0',

Thus, if you’re seeing expiration date is 19 Nov vs 11 Jan in Expires header, you can easily guess what sent the anti-caching headers (PHP vs WordPress).
At the same time it nulls the Last-Modified header (if e.g. a plugin set it).

The sending of anti-caching headers is implemented in nocache_headers() function, which is called in the mentioned areas.
You can globally override the anti-caching headers by using nocache_headers filter.

Typically, you don’t need to adjust the nocache_headers though. Their primary purpose is to instruct browsers and shared caches (like Varnish) to not cache something that should not be cached at all!

WordPress does not send Cache-Control or other cache related headers for regular pages like homepage or posts.

Which means that, in case of Varnish, the default cache TTL applies.

The de-facto standard approach to caching WordPress is adjusting cache TTL to maximum, in Varnish (e.g. 2 weeks).
This requires cache invalidation strategy, should content of an article, or website in general, change.
Varnish has no idea when you update an article contents, or change theme. Typical solution to this lies in using cache invalidation plugin like Varnish HTTP Purge. It will hook into necessary WordPress events (post update for example) and “talk” to Varnish to clear respective page’s cache upon update. Both plugin and Varnish VCL amendments required.

A slightly more flexible variation of the above approach, is having WordPress send Cache-Control headers to dictate how long regular pages are to be cached by Varnish, instead of hardcoded TTL value in Varnish. This can be achieved through a plugin like this one, which would allow setting custom cache expiration values.

E.g. to cache a page for 2 weeks in Varnish and 10 seconds in browsers, you may send:

Cache-Control: s-max-age=1209600, max-age=10

In this example we use s-max-age, which allows to specify cache lifetime for shared caches (Varnish) differently.

Last-Modified header in WordPress

WordPress implements Last-Modified for feeds only. The implementation is at ./wp-includes/class-wp.php.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: