Yahoo! Developer Network Blog

« Previous | Main | Next »


May 24, 2007

High Performance Web Sites: Rule 3 - Add an Expires Header

Web page designs are getting richer and richer, which means more scripts, stylesheets, images, and Flash in the page. A first-time visitor to your page may have to make several HTTP requests, but by using the Expires header you make those components cacheable. This avoids unnecessary HTTP requests on subsequent page views. Expires headers are most often used with images, but they should be used on all components including scripts, stylesheets, and Flash components.

Browsers (and proxies) use a cache to reduce the number and size of HTTP requests, making web pages load faster. A web server uses the Expires header in the HTTP response to tell the client how long a component can be cached. This is a far future Expires header, telling the browser that this response won’t be stale until April 15, 2010.

Expires: Thu, 15 Apr 2010 20:00:00 GMT

If your server is Apache, use the ExiresDefault directive to set an expiration date relative to the current date. This example of the ExpiresDefault directive sets the Expires date 10 years out from the time of the request.

ExpiresDefault "access plus 10 years"

Keep in mind, if you use a far future Expires header you have to change the component’s filename whenever the component changes. At Yahoo! we often make this step part of the build process: a version number is embedded in the component’s filename, for example, yahoo_2.0.6.js.

Using a far future Expires header affects page views only after a user has already visited your site. It has no effect on the number of HTTP requests when a user visits your site for the first time and the browser’s cache is empty. The impact of this performance improvement depends, therefore, on how often users hit your pages with a primed cache. (A "primed cache" already contains all of the components in the page.) We measured this at Yahoo! and found the number of page views with a primed cache is 75-85%. By using a far future Expires header, you increase the number of components that are cached by the browser and re-used on subsequent page views without sending a single byte over the user’s Internet connection.

Steve Souders

[Steve Souders is Yahoo!'s Chief Performance Yahoo!. This is one in a series of Best Practices for Speeding Up Your Web Site. This article is based on Steve's book High Performance Web Sites, published by O'Reilly.]

Posted at May 24, 2007 11:10 AM | Permalink

Bookmark this on Delicious

Comments

Will adding a unique querystring (like ?ver=[timestamp], or even ?[timestamp]) to the file also force the browser to download a fresh copy?

I see that method used frequently - it seems like that might simplify the buildscript - you don't need to worry about the version number, instead allowing the version control system to handle that task.

Posted by: Geoff Moller at May 25, 2007 9:50 AM

You wouldn't want to use [timestamp], if that's the current timestamp, because then the file will never be read from cache. The idea of Rule 3 is to increase the use of the browser cache. You could say ?ver=[versionnum], because that will be the same as long as the file hasn't changed, but that's about as difficult as changing the filename. Since its possible that some caches (proxies, CDNs) might ignore the querystring, revving the file name is preferred.

Posted by: Steve Souders at May 25, 2007 10:05 AM

Sorry - I wasn't very clear - [timestamp] would be the time at build, so it seems like caching would be possible with the timestamp.

Is there a reason proxies and/or CDNs could ignore the querystring? Are they just not necessarily built to honor that portion of the URL?

Thanks Again,

Geoff

Posted by: Geoff Moller at May 25, 2007 12:54 PM

If [timestamp] is build time that would work fine. I know Akamai can be configured to ignore querystrings, and in some cases for Yahoo! that has been done. Proxies run by private organizations have an incredibly varied (and in some cases non-conforming) configuration, so it is possible.

Posted by: Steve Souders at May 25, 2007 1:11 PM

I use the exact technique for serving all the static files. Although the versioning is made part of the url (a prefix, like a virtual directory with the build number). A Filter is configured on the virtual directory path to process the url. Also, browsers revalidate all the static resources if a "Refresh" or "Reload" button is clicked, so, one thing to note is that if a js file is composed of several small js files (could also be gzipped), the server side should be configured to check for the If-Modified-Since request header to send 304 (Not modified) response header - another level of optimization. (Typically all web servers do this, but if the content is generated dynamically i.e, if the content is not from a physical file then we need to wire this in ourselves). An E-Tag header can also be used for the 304 purpose, but IE has a bug where it does not send the E-Tag back for gzipped resources. One question I have is that the HTTP/1.1 RFC clearly states that Expires header should not be more than an year. So, is it even required to set more than an year in the Expires header or does the browsers happily accept an Expires header more than an year?

Posted by: Kishore Senji at June 4, 2007 4:59 PM

The HTTP/1.1 RFC (http://www.w3.org/Protocols/rfc2616/rfc2616.txt) does in fact state in section 14.21 that "HTTP/1.1 servers SHOULD NOT send Expires dates more than one year in the future." This is a guideline and not a requirement, and so browsers do indeed happily accept an Expires header more than a year in the future. Given the frequency with which users clear their cache and fill their cache, setting an expiration date one year or ten years in the future might not make much difference. IE users: what's the oldest "Last Accessed" date of a file (not cookie) in your cache?

Posted by: Steve Souders at June 4, 2007 9:15 PM

HTTP 1.1 specifically states that URLs containing query strings (i.e., "?") are not cacheable, though many (if not most) proxies will indeed cache them. The safest method is therefore to modify the URL, perhaps using a timestamp or version number, since that doesn't involve query strings.

Posted by: Glen Campbell at June 24, 2007 4:49 PM

Section 13.9 of RFC 2616 (the HTTP 1.1 spec) says that query strings are cacheable if there's an explicit Expires header, so I believe using a querystring is OK.

Posted by: Steve Souders at June 27, 2007 3:29 PM

My main concern is that you can't guarantee every page of your website will be included in the SERPs. Considering I'm constantly adding new products to my company's website, I need to be sure that customers can find them as soon as possible.http://www.seoptimizerz.com

Posted by: SEO at July 24, 2007 3:41 AM

Given IE's tendency to never remove anything from its cache until the user explicitly empties it, isn't changing the name of your resources every time you make a change (or a new build) to your site just a great way to fill up your end users' disk with files taht will never be accessed again? Maybe you'll make your site a fraction of a second faster by doing this, but if everyone started doing this, I would think that a lot of IE users would start to see their computers gradually slowing to a crawl (more so than they already do, anyway).

Posted by: drew at July 25, 2007 9:37 AM

Why isn't "Cache-Control: max-age" taken into consideration? Most of the time, it does the same thing as Expires, and overrides Expires if present, which means that if we added an Expires header, it would be ignored by any proper HTTP/1.1 client, but YSlow would consider it much better. :-)

Ideally, we should have both, but still, we shouldn't get dinged significantly for not having an Expires if we have a Cache-Control: max-age.

Posted by: pudge at July 26, 2007 10:57 AM

Also, we use a querystring, simply because it is far simpler to implement for us, and we don't have any CDN problems ... and any proxy that ignores it is simply, and severely, broken and if we can fix that by forcing them to deal with it, then all the better!

Posted by: pudge at July 26, 2007 11:06 AM

What about a Last-Modified header instead? It's hard to predict how much time will something stay the same, but it's much easier to tell when it did change.

http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.29

Posted by: Ralph at July 26, 2007 12:32 PM

Ralph, the Last-Modified header is great, but the browser still has to send a request and get a 304 (not Modified) response back. Using a long Expires: header removes even that requirement, so the browser happily caches the resource and never looks back. When you're Yahoo!, even 304s can be overwhelming (I guess).

Posted by: Jim Kane at July 26, 2007 2:04 PM

Yes, I second what Jim said: Expires/max-age are superior as they mean there's no need for a request. However, ETag/Last-Modified is still better than nothing.

This brings up a flaw in the grading though ... if I have no caching, I will get dinged on the Expires header, but get an A for ETag. But if I include ETags, which will not hurt performance and will usually help performance, then my YSlow grade will go DOWN. I think that's ... wrong. :-)

Posted by: pudge at July 26, 2007 3:06 PM

Maybe obvious to Apache Dons but I had to uncomment the following line

LoadModule expires_module modules/mod_expires.so

Otherwise simply adding the following line when I restarted caused a startup error

ExpiresDefault "access plus 10 years"

Posted by: Carlton Dickson at July 27, 2007 1:50 AM

It should be noted that the "ExpiresActive on" directive is necessary in addition to the ExpiresDefault directive.

Posted by: Jonathan at July 27, 2007 9:38 AM

drew: IE 6 has problems with orphaned files in the cache that never get removed (there are utilities that can remove them). I don't believe that's the case with IE 7.

pudge: Using "cache-control: max-age" instead of Expires is fine. I prefer Expires because it's more obvious and is supported in HTTP/1.0. YSlow checks for both, so you're not getting dinged. The ETag subject is complex. I really recommend reading the book, as there are 10 pages on these subjects. The summary: the default syntax for ETags is definitely bad for performance. YSlow only subtracts points if you have the default (bad) syntax. If you properly remove the "inode" from ETags for Apache and similarly remove "changeNumber" from ETags for IIS, you don't lose points.

Carlton & Jonathan: Thanks for the additions for how to properly setup the config.

Posted by: Steve Souders at July 28, 2007 10:57 AM

Yes, you can either have Apache cache gzipped files to reduce CPU, or you can do that ahead of time manually. If you work with your CDN you can usually get them to gzip your content. You might have to pay more, since they're spending more CPU time to compress your files when appropriate.

Posted by: Steve Souders at July 28, 2007 11:02 AM

Steve: I don't think that's accurate. I just checked YSlow and clicked to see the headers for files that are dinged for no Expires header, and it even shows the Cache-Control: max-age= header there.

Posted by: pudge at July 29, 2007 10:24 PM

Pudge - If the expires date (either from Expires or max-age) is less than 2 days in the future, YSlow will still subtract points.

Posted by: Steve Souders at August 20, 2007 2:31 PM

Spot the misspelling.

"If your server is Apache, use the ExiresDefault directive to set an expiration date relative to the current date. This example of the ExpiresDefault directive sets the Expires date 10 years out from the time of the request."

Posted by: Joe at August 23, 2007 1:37 PM

Can somebody explain me what's the difference between Expires and Cache-Control? I can't understand the difference between these two...

Posted by: Agustin at August 26, 2007 8:34 PM

Can I write Expires code without accessing Apache?
Can I write Expires code in HTML? in PHP?
Can you provide an example for:
a stylesheet
an image
a Flash file
a script
?

Thank you very much.

Posted by: Jonathon N at September 5, 2007 4:12 PM

We have added an expires-header to images, html, css and javascript.
Caching works fine for images, but doesn't seem to happen for css and js, although the expires-header is included in the response-header. Tested from different locations with different browsers and tools (firebug and IEWatch)

Is there anything missing in our configuration/response-header?
http://www.therapie.de/psyche/info/

Posted by: Fritz at September 18, 2007 4:53 AM

Hi, Fritz. Your Expires headers are working as expected. I believe the issue is how your testing it. When I tested your page in FF 2.0 and IE 7.0 the components were read from cache on my second page view. Specifically, I loaded your page, and then clicked on the link again. (You don't want to hit "Reload" because that tells the browser to not use the cache.)

One source of confusion is a known bug in Firebug's Net panel where stylesheets and scripts show up, even though they do not generate any network traffic. I'll write a blog about that today on YDN. I'm not sure why that would happen in IEWatch.

I recommend you test it using LiveHTTPHeaders (http://livehttpheaders.mozdev.org/). You should see that on your second page load there are no HTTP requests. I see you put a 1 day expiration date on the HTML page itself. This might be too aggressive, as it could take up to a day for users to see any update to that page.

Posted by: Steve Souders at September 18, 2007 9:33 AM

Hi
Capt newbie here...not really but stumped ow. I know how to set this header from php, its a simple call. But how do I set this for images? I assume something in the .htaccess file.
Could some kind soul include that code here for me?

thanks

Posted by: steve at September 21, 2007 12:54 PM

If I'm on a shared hosting server, is there a way I can specify I want js and css files to be cached?

Posted by: snekse at September 21, 2007 1:15 PM

I have set my expries headers to be one hour and I still get an F in the rating. Normally people visiting my site don't stay for more than an 20 mins, so I would have thought that setting this time for one hour still saves our servers in the ballpark of 80% of 304 hits. I think this would happen for most sites, yet we still get an F. How far do you need to set this in advance to get a better rating?

Posted by: Johno at September 26, 2007 2:20 AM

Hi, Johno. You're right that setting the Expires header one hour ahead will help users during their 20 minute session. But what about when they come back tomorrow? Then they have to do another conditional GET request. So there are definite drawbacks to setting it too short. Are there any benefits to a short Expires time window? One might think it allows them to change a resource without changing the actual filename of the resource. At Yahoo! we've found there are proxies that don't adhere to the HTTP spec. Our belief is, once you put a resource out in the public, you can only change it if you change the filename. With this guideline, it makes sense to set the Expires date far in the future. The spec suggestions it be no more than 3 years in the future.

As for YSlow, the Expires date must be at least 2 days in the future to avoid losing points.

Posted by: Steve Souders at September 26, 2007 9:51 AM

So just to be clear Steve, do you think I'll suffer headaches if I use a QS instead of a filename? I've been looking through my cache and can see plenty of files from yahoo (gif, js, css, jpg) from yimg.com that use the QS method, yet you suggest in your last comment (September 26, 2007 9:51 AM) that you don't think the QS method is safe?

QS is a lot easier for me, I'd rather just update a conf file in my php than have to re-save my file with a different filename I think. That said the latter isn't the end of the world, if you think there's benefit to it.

PS just got the book in the post, but it doesn't cover the QS/filename debate at all :(

Posted by: Seb at October 4, 2007 3:33 AM

Hi, Seb. Using a querystring should work. You might have misunderstood the comments. I actually said using a querystring is OK, and cited the appropriate reference in the HTTP spec. Good suggestion - I'll mention this in the 2nd edition of the book.

Posted by: Steve Souders at October 4, 2007 4:09 PM

OK, I've worked this into my site now.

To make maintaining this not a complete ball-ache, I've taken every single img/js/css filename out of my code, and replaced them with PHP constants. I then have a file (filenames.php) included on every page of the site, with the filenames in it. Thus if I change a filename, I only have to change it in one place, and not trawl through my site.

The catch is, this means I need to run my CSS through PHP, to correctly translate the echo'd php constants into filenames. As such my CSS file is now main.css.php, with the first few lines as:
--------------------------------
require('global.php'); //this in turn references filenames.php
CacheFor(365*24*60*60);
header("Content-type: text/css")


My CacheFor function is:
--------------------------------
function CacheFor($n)
{
header('Expires: ' . date('D, d M Y H:i:s',time()+$n) . ' GMT');
header("Cache-Control: max-age=$n");
header('Pragma:');
}


The empty Pragma header is in there because my apache is sending a pragma:no-cache out with all php files, and while IE ignores it in favour of the cache-control header, firefox was honouring it. I don't want to change anything in apache, because most php files I do want that header sending out, so it seems easier just to change it manually in my CSS files.

Another thing to be wary of, if for example you change an image that is in your CSS file, you'll need to rename your css file as well, or the browser won't see that it should be requesting a new image name.

I hope to write a short tutorial covering the above and more, if I get around to it I'll post a link up here.


As an aside, I've also realised that my homepage very rarely changes (a splash page introducing you to the site with some options as to where to go) so I've put CacheFor(48*60*60) in that. So if a user re-visits my homepage within 2 days it's 0bytes and 0HTTP requests. Surely that should be an A*? ;-)

Posted by: Seb at October 5, 2007 3:45 AM

PS this is the bit that I was referring to Steve:

"Since its possible that some caches (proxies, CDNs) might ignore the querystring, revving the file name is preferred."

I don't know enough about the structure of the internet and proxies to comment, but I'd rather not risk it, so I'll go with the revving filenames version I think. It just seems more "definite"!

Posted by: Seb at October 5, 2007 3:58 AM

Referring to this comment: "Also, browsers revalidate all the static resources if a "Refresh" or "Reload" button is clicked"

Does not the same hold true when you open a new browser session? I was thinking that IE is aggressive about revalidating everything the first time you hit a site or page in a new browser session. Can anyone verify? Then, when caching is set to Automatically, IE does not revalidate resources for the rest of the session.

Posted by: RobD at October 10, 2007 6:28 AM

Hi, RobD. The "Automatically" setting in IE makes it difficult to do cache testing. You can get an explanation of the heuristics behind "Automatically" from Eric Lawrence, author of Fiddler and member of the IE team here:

http://msdn2.microsoft.com/en-us/library/bb250442.aspx

Search for "The Automatically setting bears some explanation".

The results depend on many variables: Whether you open a new window vs. closing all browsers and opening a new browser. IE vs Firefox. Your cache setting. And the cache settings for each resource.

Posted by: Steve Souders at October 10, 2007 8:32 AM

The "automatic" setting is the common setting for nearly all users. That is why I am testing with it. And, the scenario I am considering is where a user opens a new browser and goes to my site. This is a common case and there seem to be some specific considerations for this scenario. Thanks for the Microsoft link. I will look at that.

Posted by: RobD at October 10, 2007 9:01 AM

This may be a dumb question, but Yslow says there should be an expires header for images, css and script files, Do you just stick a meta tag in each or in the link? Examples please.

Posted by: John Dooley at October 24, 2007 9:31 AM

Steve, you ask "Given the frequency with which users clear their cache and fill their cache, setting an expiration date one year or ten years in the future might not make much difference. IE users: what's the oldest "Last Accessed" date of a file (not cookie) in your cache?"

But surely shared caches use the Expiry date as well when they decide what to do, and we've got no way of knowing how long they keep our content.

Posted by: Tom Bradley at October 26, 2007 4:33 AM

If the shared cache is used by many users, it's still not going to make much difference. If you set it for 1 year instead of 10 years, over the course of 10 years you'd have to make 10 requests instead of 1. That's not going to be noticed across the overall population. This scenario does argue strongly for setting expiration dates that are relative to the time of the request, so it's always rolling out there as far as possible.

Posted by: Steve Souders at October 26, 2007 1:28 PM

How would the expire header affect to the index page of your site. If my index page changes continously, using an Expires header will make the updates very hard to be visible to the world. So looks like the long expiry should be set only for components that are versioned with the file name and html pages that are likely to be updated should have an expiry for one or two days ?

any thoughts here ?

Posted by: WordPress Guru at October 30, 2007 6:28 AM

Because HTML pages change frequently, they generally do not have a future Expires header.

Posted by: Steve Souders at October 30, 2007 7:13 AM

Anyone got any good tips on an actual php/apache implementation of expire headers for images? I am on a shared server, so I have little power over server settings.

I checked my phpinfo() and mod_expires is listed among the loaded modules in the apache2handler. I searched a bit, and found that I could set some settings in a .htaccess-file, so I have added an .htaccess-file with the following contents:

#set expires headers to images
ExpiresActive On
ExpiresByType image/gif A2592000
ExpiresByType image/png A2592000
ExpiresByType image/jpg A2592000
ExpiresByType image/jpeg A2592000

Now... YSlow still does not seem to find an Expires header on any images on my site and is still giving me the grade F! (boo-hoo) I notice that developer.yahoo.net's pages on "high website performance" does not have expire headers for their images either according to YSlow, so maybe there is something wrong with YSlow? :) Is there any other way I can test Expires headers on my websites images?

Posted by: Torkil Johnsen at November 22, 2007 12:49 AM

Rails has built in a random number appended to the static file as a query parameter which changes every time the file is changed. Ingenious!

E.g. http://localhost:3000/stylesheets/reset-fonts-grids.css?1185121970

the number at the end changes if it were edited.

Posted by: Julian at November 25, 2007 12:15 PM

This isn't a random number - it's an epoch timestamp, the number of seconds since Jan 1, 1970. In the URL above the epoch time "1185121970" is Sun July 22, 2007 17:32:50 GMT, which should match the last modified time of that resource.

-Steve

Posted by: Steve Souders at November 25, 2007 12:56 PM

I m new to performance tuning of web apps. I would like to set the expires parameter to images, css, js. How can I achieve it with mongrel. We are not using apache or any other webserver.

Thanks,
Vivek

Posted by: Vivek at December 3, 2007 3:33 PM

It would be nice if this rule explained somewhere it should expire more than 2 days into the future.

Posted by: Olaf van der Spek at December 8, 2007 9:45 AM

http://www.thinkvitamin.com/features/webapps/serving-javascript-fast

That suggests that incrementing a query string won't make Opera and Safari cache the files. Any thoughts on that?

Posted by: Robert at December 9, 2007 1:50 AM

Hi Steve,
thanks for this great resource. This may seem like a naive question, but in my brief experiments I came to the conclusion that most modern browsers will cache resources anyway (unless you explicitly do a browser refresh). So why would you still go to the effort of implementing this?

thanks
Brian

Posted by: Brian at December 17, 2007 9:20 AM

Hi, Brian. In your testing make sure to try the subsequent page view hours or days after the initial page view. It might be that the browser you're using coupled with the Last-Modified header are hitting an edge case, such as the Automatically setting in IE ( http://msdn2.microsoft.com/en-us/library/bb250442.aspx ).

-Steve

Posted by: Steve Souders at December 17, 2007 11:48 AM

Where do I put that "Expires: Thu, 15 Apr 2010 20:00:00 GMT" header, in the html page itself or in every js, css files? I am new to this performance stuffs. I can write that header with php header function but I have no idea where to put that code.
When I placed that code in html page which was generated from php, the whole page was cached and browser didn't ask for new file even if I change js file names.
Let me know, Thanks.

Posted by: kevin at January 10, 2008 1:39 PM

So how far out is considered a far future Expires header? 1 week? 2 weeks? For some of us, adding version numbers or serial numbers to content is not possible but to expire a page more than a week out really risks that the page will be changed by then and then you're hosed.

Posted by: Geoff M at February 4, 2008 5:39 PM

I am having a difficult time seeing why you would want to say your page will expire 2 or 3 years in the future. If the point is to increase speed (hence using a cashed version) I could see why a developer would want to put the expiration date in the future, however the risk of having a cashed version being used when fresh content has been added does not seem like a good idea to me. I know I am new at this so maybe I am missing something, but this just seems like a bad idea to me.

Posted by: wblake at February 18, 2008 8:51 AM

I checked out a few Yahoo! web pages looking for a sample of exactly what the "expires" tags would look like and didn't find anything I could use as a precise example. Some pages didn't have any expires on them at all. ???

Posted by: Dave at February 25, 2008 5:11 PM

I was psyched about caching my entire web application until I realized that I may be asking for potential security threats and copyright infringement if the right person analyzed the cached content. I'm not 100% sure but it is entirely possible that caching is a security risk for content management systems, right?

Posted by: Michael at February 28, 2008 12:25 AM

Why isn't anyone addressing the question of how to improve performance if your website is hosted on a shared hosting plan and you don't have access to the server?

In my case that would be a windows server with a website which uses ASP

Please, if anyone of you who own your own web server would look down your aristocratic nose and consider helping one of the riffraff and explain how I can use control caching.

Posted by: Phil at March 9, 2008 6:20 PM

Same problem as someone already reported above. I check and see the expire header but Y!Slow keeps saying there is none. What gives?

p/s: quite a few spam comments in here

Posted by: Son Nguyen at March 14, 2008 8:53 PM

Reading through this I see the issue of YSlow saying there is no expires header but it is in fact there.

An example:
URL: http://example.com/icon.php?f=loading

First request:
GET /icon.php?f=loading HTTP/1.1
Host: 192.168.23.51
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-GB; rv:1.8.1.12) Gecko/20080201 Firefox/2.0.0.12
Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
Accept-Language: en-gb,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Connection: keep-alive
Cookie: session=a53a865feb82dc7bbde26d49080d64d7

Response:
HTTP/1.x 200 OK
Date: Tue, 18 Mar 2008 12:08:11 GMT
Server: Apache
Last-Modified: Tue, 18 Mar 2008 00:00:00 GMT
Expires: Mon, 12 Jan 2009 23:59:59 GMT
Cache-Control: public
Content-Encoding: gzip
Content-Length: 1606
Keep-Alive: timeout=5, max=100
Connection: Keep-Alive
Content-Type: image/gif

Second request:
GET /icon.php?f=loading HTTP/1.1
Host: 192.168.23.51
User-Agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-GB; rv:1.8.1.12) Gecko/20080201 Firefox/2.0.0.12
Accept: text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5
Accept-Language: en-gb,en;q=0.5
Accept-Encoding: gzip,deflate
Accept-Charset: ISO-8859-1,utf-8;q=0.7,*;q=0.7
Keep-Alive: 300
Connection: keep-alive
Cookie: session=a53a865feb82dc7bbde26d49080d64d7
If-Modified-Since: Tue, 18 Mar 2008 00:00:00 GMT
Cache-Control: max-age=0

Response:
HTTP/1.x 200 OK
Date: Tue, 18 Mar 2008 12:09:10 GMT
Server: Apache
Last-Modified: Tue, 18 Mar 2008 00:00:00 GMT
Expires: Mon, 12 Jan 2009 23:59:59 GMT
Cache-Control: public
Content-Encoding: gzip
Content-Length: 1606
Keep-Alive: timeout=5, max=98
Connection: Keep-Alive
Content-Type: image/gif

As you can see from above the Expires header is set for 12 Jan 2009 23:59:59 GMT (or 300 days in the future). YSlow claims that the expires header is not set.

There is no consistancy in the reporting of no expires header. All of the URLs on my server have query strings and most have the same expires date. YSlow only complains about some.

YSlow also does not complain about some of the pages where the expires header is set in the past for components (images/javascript/html) which should not be cached.

Any ideas?

Posted by: albert at March 18, 2008 5:14 AM

Hi Steve,

I have tried to search on net a lot to set expire header for js/css and images but i did not get anything. I have also gone through complete discussion regarding Enxpiring but still no one has replied for issue regarding js/css/image Expire setting in jsp.There is no use of YSlow Add an Expires Header rule if we are not able to get solution of it.Everyone is eager to get A for Add an Expires Header but they dont know how to solve it. It will be really great if someone can guide to set expire time for JS/CSS/Images for JSP.

JSP code is highly appriciated.

Regards,
Jigar

Posted by: Jigar at March 20, 2008 3:02 AM

hi Jigar,
If you are using TOmcat the below is the way to add expiry date to the
js,images and css files that i figured out.

1)you need to create a filter for the above files to add an expiry date.
At the below link you will get code illustration of how we should do it
------------------------------------------------------------------------
http://www.jguru.com/faq/view.jsp?EID=1311010



2)How to make sure that the expiry date is added properly to those files?
--------------------------------------------------------------------------
I did a lot of investigatation on the above problem. you can use the following
tools
1) Yslow itself to check which compenents has expiry header and which components do not have.But it is some what buggy is not correctly reporting the expiry date of components.
2) http://www.webpagetest.org
AOL's Page test tools you can download the desktop version and for accurate picuture you can check the optimization report to know which components have expiry header which components do not have.This tool also some gives different problems.

3) The best way that i figured out is

The number of httrequests that will go to the server is equal to the number of components (js,img,css) present in the webpage.

Step1) write some debug messages( i.e printing requested url and filename) in the filter that you have written for adding expiry date.
Step2)clear cache and download the page from the browser now check whether filter has modified any response by looking at the debug messages.
The number of debug messages = number of components requested.
by debug messages you can know to which component we have added expiry header.

Step3) Load the page again (please don't refresh as per my understanding refresh will invalidates all components of the browser cache and brings fresh components from the server) by copying the url and pasting in another browser window.

Now at server you can observe that no debug messages has been printed from the filter which means that those component have not been requested from browser because they are present in the browser cache which means our expiry date is working properly.

i hope you understood the above.

regards
rama




Posted by: Rama at March 31, 2008 12:15 AM

Is there any suggestions on how to configure Websphere to set the expire header for css, js, etc. files. I tried what was suggested above in the link: http://www.jguru.com/faq/view.jsp?EID=1311010 but was not successful.

Regards,

Marty

Posted by: Marty at April 16, 2008 12:17 PM

I was wondering if you had looked at other cache-control headers such as 'public', 'private' and 's-maxage' and their effect on the caching performance end to end?

Posted by: nick holmes at April 24, 2008 6:59 AM

Hey,

For served static content I added the expiration headers, both Expires header and cache-control set to one year. When having the last-modified header ON I noticed the browser makes the If-Modified-Since request.
When removing the Last-Modified header but leaving the expiration headers, in order to get rid of the redundant request, the browser downloaded the static files allover again for each page.

Response Headers
----------------------
Date Sat, 26 Apr 2008 12:29:08 GMT
Server Apache
Accept-Ranges bytes
Cache-Control max-age=31536000, public
Expires Sun, 26 Apr 2009 12:29:08 GMT
Vary Accept-Encoding
Content-Encoding gzip
Content-Length 12489
Content-Type application/x-javascript

Is there a way to make the browser use its local cache for a whole year without validating(using If-Modified-Since)? Isn't it what "Expires" header mean?

Thanks in advance,
Aviel.

Posted by: Aviel at April 26, 2008 7:10 AM

Hi,

I have it all set up perfectly throughmy .htaccess file.
BUT there are few images and thumbnails on the page that are called from external sites (e.g. YouTube thumbnails).

Is there any way to set Expires 2 years on these *external* images???

I tried adding type="image/jpg" , thinking that then it would obey the htaccess Expiration settings. But nothing seems to change. Is there anything else that I could try?

Posted by: Garen at April 30, 2008 2:17 PM

If embedding a version number in the filename of the component, does it make sense to use the url as the ETag too?

Would make conditional http gets very simple to detect.

if (HTTP_IF_NONE_MATCH == REQUEST_URI) return 304.

Posted by: Jared Williams at June 12, 2008 8:40 AM

I just did a quick test with Expires and Cache-Control: max-age
They seem to work fine however when I modify an image the server responds with a Last-Modified date, then the next request will show the modified image, I thought the point of expires was to bypass this conditional request?? otherwise what is it even doing?

Posted by: Tj Holowaychuk at June 27, 2008 9:55 AM

Can someone comment on the need to use both Expires header and Cache-Control header? We just implemented Cache-Control and the extra 304 checks have stopped so we don't see a need for the adding an Expires header unless there's some other reason we need it.

Posted by: Mauvis at July 10, 2008 2:20 PM

Hi Steve,

Even though there is a cache-control attribute set in the header for images,scripts and style sheets in www.trovix.com, rule (3) in y slow fails miserably (noticed that i get always a rating of "f"). any comments?

PS:
we are using tomcat server and setting the cache-control using a servlet filter similar to what you have mentioned above (http://www.jguru.com/faq/view.jsp?EID=1311010)

Jay

Posted by: Jay S at August 28, 2008 3:23 PM

Instead of constantly changing the filename, why not vary the URL structure by inserting a 'version' like so http://server/images/version/filename.jpg? Do browswers/proxies cache based on the filename or the URL or both? If it's via filename, then you could theoretically have two different web sites that use the same filename (foo.jpg) and the browser/proxy would not know which to display. I like the idea of using the altered URL instead of filename so you don't need to worry about having the build/deploy process change the filename. In my case, all my images are in CSS files, so changing the filename would be a PITA.

Thanks,
-Bob

Posted by: Bob at September 30, 2008 8:39 AM

Where should I put this command? In a file in root? or where?

Thanks

Posted by: Nima at November 26, 2008 5:48 PM

Can somebody address this how can this be configured in IIS for specific file types (javascript and CSS and images only). I have Googled everything and found a million web sites about where to go, but none about specifics. I would like to configure IIS 5.1 to serve an expires headers 60 days in the future ONLY for javascript files, stylesheets, and images. I know you go to web site properties, HTTP Headers tab. However, if you Enable Content Expiration it does it for all files in the site regardless of content type. I also tried different combinations of custom HTTP headers, but nothing works. It seems to be an all or nothing approach. Can anyone help? Thanks.

Posted by: Adam at February 2, 2009 12:46 PM

I think there are problems with YSlow,
This is the code that I have on my .htacces -

Header unset ETag
FileETag None

Header set Cache-Control "public"
Header set Expires "Thu, 15 Apr 2010 20:00:00 GMT"

Header unset Last-Modified


# BEGIN WPSuperCache

RewriteEngine On
RewriteBase /mac/
AddDefaultCharset UTF-8
RewriteCond %{REQUEST_METHOD} !=POST
RewriteCond %{QUERY_STRING} !.*=.*
RewriteCond %{HTTP:Cookie} !^.*(comment_author_|wordpress|wp-postpass_).*$
RewriteCond %{HTTP:Accept-Encoding} gzip
RewriteCond %{DOCUMENT_ROOT}/mac/wp-content/cache/supercache/%{HTTP_HOST}/mac/$1/index.html.gz -f
RewriteRule ^(.*) /mac/wp-content/cache/supercache/%{HTTP_HOST}/mac/$1/index.html.gz [L]

RewriteCond %{REQUEST_METHOD} !=POST
RewriteCond %{QUERY_STRING} !.*=.*
RewriteCond %{HTTP:Cookie} !^.*(comment_author_|wordpress|wp-postpass_).*$
RewriteCond %{DOCUMENT_ROOT}/mac/wp-content/cache/supercache/%{HTTP_HOST}/mac/$1/index.html -f
RewriteRule ^(.*) /mac/wp-content/cache/supercache/%{HTTP_HOST}/mac/$1/index.html [L]

# END WPSuperCache


# BEGIN supercache

AddEncoding gzip .gz
AddType text/html .gz


SetEnvIfNoCase Request_URI \.gz$ no-gzip


Header set Cache-Control 'max-age=300, must-revalidate'


ExpiresActive On
ExpiresByType text/html A300

# END supercache

And I have wordpress on,
I am checking with YSlow And I get -
52,
D on ETag
F on Expires Header

What to do??

Posted by: Yosy at March 11, 2009 8:29 AM

Hello every one
I want to know if i add an expires header, and after few days i have some changes in my CSS or JavaScript file, Is it possible that i write past date in "Add an expires header" so that already cashed files could be ignored?

Posted by: rafiq at March 20, 2009 12:00 AM

I saw your saying that we must change the file name if we are using a far expiration time.

this means a search engine wont crawl the page again if the time is far? wont check for new components and wont understand there is a change happened?

Posted by: شات at March 23, 2009 10:18 AM

I'm on shared hosting and am proficient in html, css and php along with a couple of software programming languages. I've reviewed the documentation on expired headers for my css and image files and want to implement. None of the documentation I've seen include the necessary information, which is "where actually do I go and what is the syntax if I'm a webmaster but not a server admin to make these changes?"

What I really need to know is:

1) is this an edit to the htaccess file or is it implemented in the body of the (for example) css file? or somewhere else? If so, which files are edited, are they files available to a shared hosting user, and what is the syntax.

2) the only access I have to apache on my shared hosting server is through apache handlers in cpanel, which I don't know how to use and can't find accessible documentation on. Can these changes be made using this utility in cpanel?

I realize at least 4 other people have asked this same question on this thread with no results, so the remaining posters must think these questions are silly newby questions, but even a brief response telling us it can't be done in shared hosting if that's the case would be helpful.

Thanks.

Posted by: Sophia at April 30, 2009 1:05 PM

For those that asked the question as to what minimum value to set for 'far future Expires' but never got an answer; as far as YSlow is concerned, to get an A-grade pass, the minimum is 3 days but the best solution appears to be, go for a much longer expires setting and version control the filenames of semi-static content.

Posted by: Willabee at May 12, 2009 3:55 AM

Someone has already asked this, but there are external images to my page as well... is there a way to add expires header for them?

By adding an expires header for image/jpeg i can set it for all images from my site but not from others. Any help will be appreciated.

Posted by: Mukesh at May 14, 2009 6:16 PM

I have written a quick blog post / walkthrough on how to do this available on our blog here:

http://blog.dynamic50.com/index.php/2009/05/using-yslow-apache-and-passenger-to-make-your-site-faster-stage-1-expiries/

Posted by: Jason Green at May 22, 2009 5:51 AM

Hi - for those of wondering what's going on with their htaccess rules...

... it seems my web hosts are striping/overriding any cache control headers I may be applying (I'm using a shared server setup on both these hosts). I ran YSlow by mistake on my local development server to find it does actually work (unlike the W3C validator for example) and got a good grade. And it's good grades we're all after right?

Anyway - will ping off a support request to these hosts to confirm from them...

Posted by: Gabriel de Kadt at June 9, 2009 5:00 AM

Mmm - not not my hosts at all but a bug in my IDE (I hope I can pass that one of on a glitch). Restared said app (and my brain) and now updates to htaccess are working fine - please ignore my last post!

Posted by: Gabriel de Kadt at June 10, 2009 4:02 AM

Regarding using QueryString vs Renaming filename

Well, if its TRUUULY static, i prefer Renaming over QueryString.

1) It helps with MY version control. This is not an exact case of covention over configuration but if your controller can dynamically append a QS in a view, why cant the filename be dynamic too. What's the different between mapping a versionnumber and mapping a filename

2) If u dont need to do anything dynamic, even better, the file can be served without going tru a controller.

3) Mr Steve Souders probably got it right when he talks about CDN/proxies could ignore QS.

CDN/Cloud distribute/mirror ur static files and replicate over all their network in the world especially for different region for you to reduce the routing to HELP you. Trust me, they are going to ignore the queryString for some reasons.

CDN does mirroring, not caching, the physical media files. Dont get me wrong. Having a queryString will not break the CDN. It will still work and go for the newer version, only that when cdn detects a queryString, it will do a FULL route in case your apps is interested in the queryString and you will not get benefit of CDN which helps to serve users files from server that are nearer to them.

This is transparent to both webapps and users therefore it always goes undetected and people will "hey look, it works with Querystring too, who say CDN doesnt?"

We will know more as he promised to include this in his 2nd edition.

Posted by: Chan Kun Juan at June 12, 2009 6:54 AM

Sorry for appending:

In fact there are worse scenerio some CDN default itself to ignore queryString unless u explicity put a "?cache=false" and again explicity configure CDN to recognise that QS and not to serve from cache. I think this was the case with Akamai few years ago, and thats what Drupal's akamai module did. I am not sure about now.

There are alot of CDN and they varys from one and another. Quoting Steve Souders "CDN doesnt necessarily adhere by the HTTP specs". They don't have to oblige, they are not not browser.

Renaming is safer than querystring.

Posted by: Chan Kun Juan at June 12, 2009 7:45 AM

Hi All,

Can someone explain what's the with versioning css files using ? querystring when doing something like this as found on this very page:

href="http://l.yimg.com/a/combo?/yui/2.6.0/build/reset-fonts-grids/reset-fonts-grids.css&/yui/2.6.0/build/menu/assets/skins/sam/menu.css&/yui/2.6.0/build/button/assets/skins/sam/button.css&/yui/2.6.0/build/container/assets/skins/sam/container.css&/yui/2.6.0/build/datatable/assets/skins/sam/datatable.css&/ydn/site/ydn-76114.css&

As you can see it uses http://l.yimg.com/a/combo? querystring, does that mean CDN will cut this off?

Also it's interesting to note on yui site: http://developer.yahoo.com/yui/ it uses /yui/assets/yui.css?v=3

I noticed too that Squid proxy server now allows caching of ? querystring by default whereas before it was off by default so does it really matter in the end?

Rob

Posted by: Rob Smith at July 1, 2009 6:49 AM

Hi,

I've have tested with Nginx, passenger, Ruby on Rails and also Apache2, passenger, Ruby on Rails. Setting the Expires headers seems to work fine according to YSlow and Google Page Speed but when checking the web server logs, I still see requests coming in and being answered with a 304 Not modified.

Does anybody know why this happens?

Posted by: phuesler at July 8, 2009 4:54 AM

Rob Smith, you aren't yahoo. CDN can customize for yahoo.

Posted by: Ricky at July 8, 2009 5:01 PM

@Rick: Sorry what I meant to say was would the proxy servers cut the ? querystring stuff off as suggested by many people in the comments.

Thanks for replying

Posted by: Rob Smith at July 8, 2009 5:26 PM

@Rick: Sorry what I meant to say was would the proxy servers cut the ? querystring stuff off as suggested by many people in the comments.

Thanks for replying

Posted by: Rob Smith at July 8, 2009 5:27 PM

Does ySlow understand "Expires" headers when they are set as an Epoch number?

Posted by: Toby at July 8, 2009 11:13 PM

Rob Smith, I believe proxy server doesn't differentiate static data from dynamic data. If they do cut off, then it will be a mess all the REST and GET won't work anymore.

Are you worried because you are using querystring to version your static files?

Querystring might still work fine. If I did not understand wrong from the recent posts here. The only setback of using querystring is that it can potentially defeat the purpose of CDN which don't cache the same way as web browsers do.

Posted by: Ricky at July 9, 2009 5:28 PM

Hi,
I'm doing MSc degree in Performance Evaluation of websites and my expirement website is a browser-based online game called The Godfather on this url: thegodfather2.3rbcool.net

The problem is, I optimized most of YSlow rules and the grade is B but it still quite slow response time for the pages. However, some times even when the HTTP request size is 1 kb it still takes about 3 seconds to response.

here is a username and password to use if anyone want to have a look:
email: a@a.a
pass: 111111

Posted by: Talal Ebdah at July 25, 2009 4:04 PM

Rob,

I work for Limelight Networks and can assert that we allow the customer to decide whether or not to treat URL query params as significant. Content versioning by (virtual) directory or file naming is more manageable however, as troubleshooting cacheability becomes cumbersome when there are so many factors at play, and developers often add query params (such as session ID or other request-scoped parameters) without being aware of the potential effect it will have on cacheability.

Best Regards,

Jason Hofmann

Posted by: Jason Hofmann at November 2, 2009 6:57 AM

Hello..

Sorry, I am new to this expire setting. Is there any way we can set the expires for images, javascripts, stylesheets accessed over HTTPS? We got a grade A when we access over HTTP but the same over HTTPS scored a F.

Maybe HTTPS does not cache at all, in that case can we disable the scoring for that section?

Thank you!

Regards,
Clement

Posted by: Clement Chong at December 2, 2009 12:06 AM

Hi,

I've set this up and using yslow it tells me that all the files expire in 10 years. However it's still making the 304 requests. Anyone know what I'm doing wrong here?

Posted by: William Hubert at December 16, 2009 7:48 AM

Regarding HTTP vs. HTTPS, some browsers are configured to not cache any HTTPS content. You may be using a browser setup this way and not even know it, if your company has a default policy of configuring browsers this way.

Posted by: Brian at December 16, 2009 12:26 PM

I'm having trouble with the expires header as well.
When I develop / beta-test a website I usually have them at 30 mins, and on a finished website I set the expire header to 3 days (72 hours).

Why am I getting a D on YSlow at the "Add Expires headers" section ?
YSlow does see the expire dates.

Posted by: Daniel at February 9, 2010 10:21 AM

Post a comment

Comment Policy: We encourage comments and look forward to hearing from you. Please note that Yahoo! may, in our sole discretion, remove comments if they are off topic, inappropriate, or otherwise violate our Terms of Service. Fields marked with asterisk '*' are required.

Remember Me?

Subscribe

YDN Blog: Get Yahoo! Developer Network Blog on your personalized My Yahoo! home page.

Add To My RSS Feed

YDN Link Blog: Get Yahoo! Developer Network Linkblog on your personalized My Yahoo! home page.

Add To My RSS Feed

Recent Readers

Copyright © 2010 Yahoo! Inc. All rights reserved. Copyright | Privacy Policy

Help us continue to improve the Yahoo! Developer Network: Send Your Suggestions