Skip to main content
Ben Nadel at the New York ColdFusion User Group (Jul. 2008) with: Michael Dinowitz
Ben Nadel at the New York ColdFusion User Group (Jul. 2008) with: Michael Dinowitz

Caching ColdFusion Pages With Expires Header Value

By
Published in Comments (14)

A while back on this blog, I talked about taking ColdFusion date/time stamps and converting them to Greenwich Mean Time and then to HTTP time strings. I had mentioned that I was doing this because I was dealing with HTTP headers which required HTTP time strings; but, I never really got into then, so I figured I would talk a bit about it now. At the time, I was using ColdFusion templates to generate CSS and JS files. Because there is no inherent caching of CFM pages, the browser was continually pulling down these CFM-based CSS and JS files for every page request. To prevent this from happening, I had to manually set Expires headers to let the browser know that these ColdFusion files were cachable.

Adding an Expires header to a page response is extremely easy in ColdFusion thanks to the CFHeader tag and the GetHTTPTimeString() function. CFHeader, of course, allows us to set HTTP header values and GetHTTPTimeString() will give us the GMT/HTTP time string of a ColdFusion date for use in those header values. To prevent the browser from pulling down my CFM-based CSS file, all I have to do is add the CFHeader tag before flushing the content:

Styles.cfm

<!--- Let's have this page persist on the client for a day. --->
<cfset dtExpires = (Now() + 1) />

<!---
	Get the HTTP GMT time string for the given expiration
	date (this is the date/time format expected by the Expires
	header value.
--->
<cfset strExpires = GetHTTPTimeString( dtExpires ) />

<!--- Set the expires header. --->
<cfheader
	name="expires"
	value="#strExpires#"
	/>


<!--- Define the type of content we are sending. --->
<cfcontent type="text/css" />

<!--- CSS Content ----------------------------------------- --->

<cfoutput>

	p##target:after {
		content: "#TimeFormat( Now(), "hh:mm:ss" )#"
		}

</cfoutput>

You'll notice in the above code that the template is set to expire one day after it was served up. The beauty of this (if you can see it that way) is that it puts the onus of caching on the client (browser) and not on the ColdFusion server. Normally, if we wanted to "optimize" this situation, we'd have ColdFusion process the CFM file and actually burn out a flattened CSS file that the browser would link to directly (thereby processing the CFM file only once per application initialization); but, by making the browser responsible for caching, we don't have to jump through any hoops on the server. I'm not saying this is better, I'm just pointing out one nicety.

While it is awesome that the client is now caching our CFM page, this can present another problem - if the application is re-initialized, how can we get the client to pull down a new copy of the CFM file before it expires? The easiest way that I've found to deal with this is to version the CFM file based on the date and time the application was initialized. Take a look at the test page that links to the above CFM file:

Index.cfm

<cfoutput>

	<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
	<html>
	<head>
		<title>Setting Expires Header in ColdFusion</title>

		<!---
			When linking "static" ColdFusion assets, add the
			application version key to the URL so that the
			browser sees it as a new file every time the app
			is re-initialized.
		--->
		<link
			rel="stylesheet"
			type="text/css"
			href="./styles.cfm?v=#APPLICATION.VersionKey#">
		</link>
	</head>
	<body>

		<h1>
			Setting Expires Header in ColdFusion
		</h1>

		<p id="target">
			The LINK tag should only pull down the linked CSS file
			if it has expired, does not exist locally, or we are
			hard refresshing the apge.
		</p>

		<p>
			<a href="./index.cfm">Soft refresh</a>
		</p>

	</body>
	</html>

</cfoutput>

If you look at the LINK tag in the header, you'll see that the CFM file we use for our CSS takes one URL parameter:

./styles.cfm?v=#APPLICATION.VersionKey#

This "v" value is a numeric representation of the date/time on which the application was initialized. Because this value is tied to initialization, it is unique to each "reset" of the application. This also means that every time the application is reset, the browser will see a "new" URL in the LINK tag which it will request since it does not yet have it cached locally.

Using this version URL parameter, we get the best of all worlds - we have ColdFusion dynamically generating our CSS file; we have our client caching our CFM page to prevent subsequent requests; and, we have a way to force the client to pull down a new version of the CFM page after application re-initialization. All we have to do is make sure that our VersionKey is unique to the application reset, which is quite simple:

Application.cfc

<cfcomponent
	output="false"
	hint="I provide page request settings and application leve event handlers.">

	<!--- Define the application. --->
	<cfset THIS.Name = "expires-#Hash( GetCurrentTemplatePath() )#" />
	<cfset THIS.ApplicationTimeout = CreateTimeSpan( 0, 0, 5, 0 ) />


	<cffunction
		name="OnApplicationStart"
		access="public"
		returntype="boolean"
		output="false"
		hint="I initialize the application.">

		<!---
			Store the date / time that the application was
			initialized.
		--->
		<cfset APPLICATION.DateInitialized = Now() />

		<!---
			Create a numeric representation of the
			initialization date/time stamp. This will be used
			to version our linked assets.
		--->
		<cfset APPLICATION.VersionKey = (APPLICATION.DateInitialized * 1) />

		<!--- Return out. --->
		<cfreturn true />
	</cffunction>


	<cffunction
		name="OnRequestStart"
		access="public"
		returntype="boolean"
		output="false"
		hint="I initialize the page request.">

		<!--- Check to see if we need to initialize the app. --->
		<cfif StructKeyExists( URL, "reset" )>

			<!--- Manually execute initialization. --->
			<cfset THIS.OnApplicationStart() />

		</cfif>


		<!--- Define page request settings. --->
		<cfsetting showdebugoutput="false" />

		<!--- Return out. --->
		<cfreturn true />
	</cffunction>

</cfcomponent>

If you look in the OnApplicationStart(), you'll notice that the VersionKey is just a numeric representation of the DateInitialized value which is only set once per application initialization.

So anyway, that's the reason I was looking into HTTP time strings a while back - I needed to prevent the browser from continually pulling down "static" ColdFusion files. I am sure that you could have used the CFCache tag to do the same exact thing; but, I prefer this method as it feels more explicit and tangible for my caveman brain. Furthermore, I don't want people to get the idea that this methodology is always better than caching files server-side - if your "static" ColdFusion file is doing a lot of processing (such as minifying a set of Javascript files), then that is something you'll probably want to process only once per application initialization. The technique that I'm describing is more for partially dynamic ColdFusion files used to generate static files.

Want to use code from this post? Check out the license.

Reader Comments

15,902 Comments

@Johan,

I'll have to look into more of the caching headers. From my brief exploration, it seemed that "Expires" was the universally accepted one and that others were not official. But, that could be (and probably is) way off-base. Thanks for the link.

113 Comments

Here's how caching works with HTTP 1.1:

When a client first requests your CSS file, you will want to send down ETag and Last-Modified headers along with a 200 and the full content.

On subsequent requests, the client will send up ETag and If-Not-Modified headers, which you will need to analyze to determine whether to respond with a 304 Not Modified and no content or to respond with a 200 and full content.

You will still need to set the Cache-Control header, and the rules are a little complicated about how to format ETag and Last-Modified and how to analyze them when the client sends them up, so you will want to look into this further.

113 Comments

The Expires header works. Browsers will do as you instruct. But that's probably not what you want them to be doing, unless you are sure that the cacheable content on your server will never change, or that it doesn't matter if the content changes but the browser decides not to go get the new content and instead use the old content. Using Last-Modified and ETag instructs the browser to ask the server if there is new content available, but only to send back the content if there is new content available (and the browser will use cached content if there isn't new content available).

22 Comments

I've used the same method of passing a url parameter to signify when the file has changed, but it never occurred to me to set it to a value that was updated every time you re-initialize the application. That's darn right brilliant. Good tip. Thanks Ben!

15,902 Comments

@Justice,

And should I set Last-Modified using CFHeader in the same way? Or this something you would have to handle at the IIS level?

@Adam,

Thanks my man. Yeah, I find this to be especially useful with even standard CSS files to prevent the browser from caching old files after large updates.

113 Comments

ColdFusion, like many application frameworks and application servers out there, permits you to set any HTTP status, HTTP headers, and HTTP content that you would like to set for dynamically generated content. I'm not sure if ColdFusion has automatic capabilities for instructing the browser to cache content, but if it doesn't, you can certainly use its raw HTTP capabilities manually to instruct the browser to cache content. I wouldn't use IIS here if the content could change based on, for example, new database records, because IIS cannot be aware of such things - it only monitors whether the contents of the requested files have changed, where in this case they haven't.

1 Comments

Hi Ben,

I put similar functionality into Combine.cfc, only it uses Etags rather than expires headers. These are actually very easy to work with, and the content only 'expires' if the clients hash is different to the server's hash, it's not time based. I generate the hash by (in simple terms) hashing the last modified date of the files.

The code is quite straightforward, it may help http://combine.riaforge.org/

34 Comments

I have a thought so it might be dangerous.

What if you wanted to cache a page for all users eg.) Contact Me Page -- Being that this page wouldn't frequently change for all users how could you cache that page? Does CF have that type of feature built in? I'm sure I could code a caching system but I was just trying to save myself a few minutes of coding.

15,902 Comments

@Jody,

If you want to cache a page, I believe you can use the CFCache tag to cache a rendered page on the server. I have not used it personally, but I believe that is what it's for.

11 Comments

I wanted to add a note that this works for cfheader, but not for a cfscript solution utilizing getPageContext().

Here's my example written entirely in cfscript

<cfscript>
context = getPageContext();
context.setFlushOutput(false);
response = context.getResponse().getResponse();
out = response.getOutputStream();
response.setHeader("Pragma", "cache");
response.setHeader("Cache-Control","public, max-age=3600");
response.setDateHeader("Expires",gettickcount() + 31536000000);
out.flush();
out.close();
</cfscript>

This code may not work since I cut out the parts that process the binary data for output. The takeaway here is that setDateHeader() needs a integer of the milliseconds. 31536000000 is the number of milliseconds in one year, so this expires header should be one year in advance.

15,902 Comments

@Drew,

Cool stuff; once you start reaching into the underlying response, I always get a bit lost on which response we're actually dealing with. I think there's a "character" response that sits on top of a "binary" response and I'm not sure if they all respond the same way.

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel