The Three Caches of Forem

A Quick Tour of Our Favorite Performance Enhancing Pain Point

In the Forem code base, we make extensive use of various caching strategies. And as with any cache, we always run the risk of not invalidating the right caches.

The three caching strategies that I’ve found are:

  • Edge Caching
  • Rails Caching
  • Database Caching

At a high level, we leverage caches to improve performance. Let’s delve into each strategy. But before we do, let’s introduce a few concepts.

Response Documents and Fragments

The response document is a single file sent from the server in response to a request.

As I’m walking through this, remember that what you see rendered in your browser is almost always from the combination of many response documents sent from the server: Hypertext Markup Language (HTML 📖) documents, Javascript files, Cascading Stylesheet (CSS 📖) files, and XMLHttpRequest (XHR 📖) responses that modify the Document Object Model (DOM 📖).

To build a single response document, the server often assembles multiple fragments to form that singular document. In Ruby on Rails 📖 this is done with like layouts, views, and partial views.

Back to Listing the Caching Strategies

Let’s now work from the broader caching strategies to the more narrow ones.

Edge Caching

Edge caching is about trying to put as many of the response documents as close to the client as possible to reduce latency on content delivery.

At Forem, we enable the usage of either Fastly or Nginx. When we bust the edge cache, we are evicting specific response documents.

The next time someone requests that response document the server will reassemble it (from the various constituent parts).

You can see our Edge Busting strategy here.

Rails Caching

We use the Rails cache to store all kinds of things, mostly fragments that we use to build the response document.

Each entry we put into the Rails cache has a unique key. When we use the cache, we check if the key exists. If so we use what’s in the cache for that key. Otherwise, we run the cached logic and put it in the cache with that key.

What does that key look like? It depends. But in a general sense it often includes a timestamp. Let’s create a quick and arbitrary example.

Let’s say it is very expensive to generate the HTML for an article’s card. We choose to cache the article’s card’s HTML. The key for that might like like the following: article-<article_id>-<article_last_updated_at>

Then when we check the cache, we use the article’s ID and when it was last updated. In this way, older updates to the article might still be in the cache, but we’re not going to fetch those.

There are more complicated strategies we use. As part of the site’s layout we cache information; the community name, tracking analytics, last deploy date, last commit id, and more.

When an admin makes a site wide change, that prompts to use a new key; so long as we remember to update the attributes in the key. This pull request resolved one of the issues (in a not yet available in production) when we didn’t update the attribute keys.

You can learn more about Caching with Rails over at the Rails Guides.

Database Caching

The last is database caching. We cache an article’s tag list, user, and organization information (if applicable). These are cached as fields on the articles table. The purpose of these cached attributes is performance. Where possible, we prefer to minimize joins for queries that we frequently perform (e.g. get me a list of articles being a very common query for a Forem).

I reported an issue where changing an organization’s image should invalidate the associated articles cached organization (Issue #17041). The solution is whenever we update an organization we should revisit each article associated with that organization.

We already do that for users, but need to now account for users. Were I writing this, I would have the inner part of .find_each be a method on the article. After all the article should have knowledge about it’s cache, much more so than the user who wrote the article.

Way-finding in the Era of Caching

To put a spin on a phrase from Shrek, “Caching is like onions, it has many layers. And it will cut you and you’ll cry.” When I was working to help triage the situation that prompted my writing of Issue #17041, my initial mental model was that it was either an Edge or a Rails cache.

We tried the various tactics to invalidate those caches but continued to see the problem. Because we didn’t update the attributes we were using as a database cache.

To properly troubleshoot a caching issue, a recommendation for my future self is to ask for a screenshot and an arrow pointing to the specific thing that isn’t updating. That way I can begin sleuthing as to which caching strategy is misbehaving.