$ curl -I 'https://en.wikipedia.org/w/load.php?modules=startup&only=scripts&skin=vector&*' HTTP/1.1 200 OK Server: nginx/1.9.2 Date: Sun, 12 Jul 2015 22:44:22 GMT Content-Type: text/javascript; charset=utf-8 Connection: keep-alive X-Powered-By: HHVM/3.6.1 X-Content-Type-Options: nosniff Cache-control: public, max-age=300, s-maxage=300 Vary: Accept-Encoding Expires: Sun, 12 Jul 2015 22:49:22 GMT ETag: W/"b7xnhKt7" X-Varnish: 371412287, 1198588436 Via: 1.1 varnish, 1.1 varnish Age: 0 X-Cache: cp1054 miss (0), cp1067 frontend miss (0) Strict-Transport-Security: max-age=31536000 Set-Cookie: GeoIP=:::::v6; Path=/; Domain=.wikipedia.org X-Analytics: https=1 Set-Cookie: WMF-Last-Access=12-Jul-2015;Path=/;HttpOnly;Expires=Thu, 13 Aug 2015 12:00:00 GMT
(wait a few minutes)
$ curl -I 'https://en.wikipedia.org/w/load.php?modules=startup&only=scripts&skin=vector&*' HTTP/1.1 200 OK Server: nginx/1.9.2 Date: Sun, 12 Jul 2015 22:47:46 GMT Content-Type: text/javascript; charset=utf-8 Connection: keep-alive X-Powered-By: HHVM/3.6.1 X-Content-Type-Options: nosniff Cache-control: public, max-age=300, s-maxage=300 Vary: Accept-Encoding Expires: Sun, 12 Jul 2015 22:49:22 GMT ETag: W/"b7xnhKt7" X-Varnish: 371412287, 1199144708 1198588436 Via: 1.1 varnish, 1.1 varnish Age: 204 X-Cache: cp1054 miss (0), cp1067 frontend hit (7) Strict-Transport-Security: max-age=31536000 Set-Cookie: GeoIP=:::::v6; Path=/; Domain=.wikipedia.org X-Analytics: https=1 Set-Cookie: WMF-Last-Access=12-Jul-2015;Path=/;HttpOnly;Expires=Thu, 13 Aug 2015 12:00:00 GMT
In the first response, the Expires timestamp is exactly 5 minutes (=300 seconds, and Cache-Control has max-age=300) after the Date timestamp. It's also clear from the X-Cache header that the first response is a cache miss (even though I used the most popular load.php URL in existence; but it seems few people hit it without an Accept-Encoding header).
In the second response, the Expires timestamp is the same as in the first response, and is much less than 5 minutes after the Date timestamp. According to the X-Cache header it's a cache hit (as you can also tell from the X-Cache header, I sent 6 more requests in between that are not shown here). So it appears that the Expires timestamp is part of the data that is cached, and will frequently be stricter than the Cache-Control header.
This would be a problem if clients obey the Expires header instead of the Cache-Control header in cases like these where the Expires header is stricter. Clients would cache responses for shorter periods of time than we intend, and their revalidations (INM/304 cycles) would not be randomly distributed over time, but would be clustered right after the timestamp in the Expires header. It seems at least Chrome does in fact behave this way: I have seen it send an INM request for a cached response whose Expires header had expired even though the max-age duration had not yet expired.
I suspect this happens because we explicitly set the Expires header in ResourceLoader. We should consider not doing this, and letting Varnish compute it instead (if we decide we need Expires at all; how many clients don't understand Cache-Control these days?).