Poor man's cache

July 14, 2022 on Kyoto Framework

There are several ways to speed up pages loading on your website. One of them is page caching. Most of the caching tutorials cover only static pages/websites, without any authentication. But most websites are built with authentication in mind, like services or e-commerce web pages. In this guide, I would like to describe a $0 cost Cloudflare trick to cache dynamic pages with cache bypassing on authentication.

Please note, this guide is not suitable for projects with a low control over headers.

Why Cloudflare caching instead of your own?

The answer is very simple - <100ms transfer time. Edge caching allows serving responses from the servers near the end users. You will see your page almost immediately, as if you were using PWA.

Why doesn’t Cloudflare support this out of the box?

In fact, Cloudflare have support, but not out of the box. It needs some extra configurations, including paid one (cache bypassing on cookie is a Business/Enterprise plan feature only). If it enabled this feature out of the box, users would get content that wasn’t up-to-date and developers would have to look for this non-obvious behavior.

So, how to use cache bypassing, if it’s a paid plan feature?

It’s possible to use a combination of another Cloudflare provided functionality to achieve the same effect:

From this moment, our html pages will be cached by Cloudflare as well. But first, we must set cache settings on our side with a Cache-Control header. Without this, the cache won’t work, since we took a full control over caching with a header.

How to make it work?

All code examples will be provided using Go and built-in net/http. Anyway, it can be ported to the backend of your choice without any extra complications.

First, let’s go through the mentioned points

Configure statics caching

We will wrap a static resources handler with a custom cache controlling middleware.

// This middleware will wrap an original static handler
func MiddlewareStaticCache(h http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// Cache asssets for a 6 hours, on both browser and edge
		w.Header().Set("Cache-Control", "public, max-age=21600, s-maxage=21600")
		// Serve original handler
		h.ServeHTTP(w, r)
	})
}

func main() {
	// Determine a static serving source
	fs := http.FileServer(http.Dir("./static"))
	// Wrap static serving source with a caching handler
	fs = MiddlewareStaticCache(fs)
	// Attach static server on /static/ route
	http.Handle("/static/", http.StripPrefix("/static/", fs))
}

Configure page caching

We will do almost the same thing we did for a static serving, but with extra configuration option. This middleware will be a bit different, because we need to avoid caching on the browser side (it leads to authenticated state ignoring), ignore caching for requests with “no-cache” query and non-GET methods.

// This middleware will wrap a page handler
func MiddlewarePageCache(duration time.Duration, next http.HandlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		// Cache page for a provided duration if matching needed conditions.
		// Using max-age=0 to avoid browser caching.
		if !strings.Contains(r.URL.RawQuery, "no-cache") && r.Method == "GET" {
			w.Header().Set(fmt.Sprintf("Cache-Control", "public, max-age=0, s-maxage=%.0f", duration.Seconds()))
		}
		// Serve original handler
		next(w, r)
	}
}

func HandlerPageIndex(w http.ResponseWriter, r *http.Request) {
	// Page rendering
	// ...
}

func main() {
	...
	http.HandleFunc("/", MiddlewarePageCache(6 * time.Hour, HandlerPageIndex))
}

(!) Please note, you will need to wrap every page with this middleware. Even if you don’t need a page to be cached. If a Cache-Control is not set, Cloudflare is setting an undocumented default value. Literally, it’s an undefined behavior. To ignore caching, use zero duration argument.

Now, we need to write a flag cookie on authorization to inform our transform rule to use no-cache query.


func HandlerAuthorization(w http.ResponseWriter, r *http.Request) {
	// Process your authorization
	// ...

	// Set cookie flag
	http.SetCookie(w, &http.Cookie{
		Name:		"authorized",
		Value:		"true",
		Path:		"/",
		Expires: 	365 * 24 * time.Hour
	})

	// Page/response rendering
}

Testing

Unfortunately, you can’t check edge caching behavior locally. You’ll have to deploy changes to check everything works fine. I recommend to check behavior on staging first, then choose a right time with the lowest traffic for a production deployment.

Any real-life examples?

So far, only one, for which I found this way of edge caching. It’s a Real Estate platform which has a ~30% improvement in loading speed compared to classic server caching. You can check by yourself with going to the mybrokerone.com.