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
nocache:
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
private:
Expires: Thu, 19 Nov 1981 08:52:00 GMT
Cache-Control: private, max-age=10800, pre-check=10800
private_no_expire:
Cache-Control: private, max-age=10800, pre-check=10800
public:
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
.