Section 14.21 of the HTTP standard deals with the Expires header. The purpose of the Expires header is to determine a point in time after which the document should be considered out of date (stale). Don't confuse this with the very different meaning of the Last-Modified header. The Expires header is useful to avoid unnecessary validation from now until the document expires, and it helps the recipients to clean up their stored documents. Here's an excerpt from the HTTP standard:

The presence of an Expires field does not imply that the original resource will 
change or cease to exist at, before, or after that time.

Think carefully before setting up a time when a resource should be regarded as stale. Most of the time we can determine an expected lifetime from "now" (that is, the time of the request). We do not recommend hardcoding the expiration date, because when we forget that we did it, and the date arrives, we will serve already expired documents that cannot be cached. If a resource really will never expire, make sure to follow the advice given by the HTTP specification:

To mark a response as "never expires," an origin server sends an Expires date 
approximately one year from the time the response is sent.  HTTP/1.1 servers SHOULD 
NOT send Expires dates more than one year in the future.

For example, to expire a document half a year from now, use the following code:

$r->header_out('Expires',
               HTTP::Date::time2str(time + 180*24*60*60));

or:

$r->header_out('Expires',
               Apache::Util::ht_time(time + 180*24*60*60));

The latter method should be faster, but it's available only under mod_perl.

A very handy alternative to this computation is available in the HTTP/1.1 cache-control mechanism. Instead of setting the Expires header, we can specify a delta value in a Cache-Control header. For example:

$r->header_out('Cache-Control', "max-age=" . 180*24*60*60);

This is much more processor-economical than the previous example because Perl computes the value only once, at compile time, and optimizes it into a constant.

As this alternative is available only in HTTP/1.1 and old cache servers may not understand this header, it may be advisable to send both headers. In this case the Cache-Control header takes precedence, so the Expires header is ignored by HTTP/1.1-compliant clients. Or we could use an if...else clause:

if ($r->protocol =~ /(\d\.\d)/ && $1 >= 1.1) {
    $r->header_out('Cache-Control', "max-age=" . 180*24*60*60);
}
else {
    $r->header_out('Expires',
                   HTTP::Date::time2str(time + 180*24*60*60));
}

Again, use the Apache::Util::ht_time( ) alternative instead of HTTP::Date::time2str( ) if possible.

If the Apache server is restarted regularly (e.g., for log rotation), it might be beneficial to save the Expires header in a global variable to save the runtime computation overhead.

To avoid caching altogether, call:

$r->no_cache(1);

which sets the headers:

Pragma: no-cache
Cache-control: no-cache

This should work in most browsers.

Don't set Expires with $r->header_out if you use $r->no_cache, because header_out( ) takes precedence. The problem that remains is that there are broken browsers that ignore Expires headers.