Harnessing Client-Side Caching in Rails: The Power of expires_in

In your journey as a software developer, you'll encounter myriad tools and techniques designed to optimize and streamline web applications. One such tool, often overlooked, is client-side caching. As backend and full-stack engineers, we spend hours crafting complex caching layers on the backend. However, we often overlook the most effective caching technique: having the request never reach the backend! Let's delve into how requests get cached on the client using the Cache-Control header.

Cache Control Header

At the core of client-side caching is the HTTP Cache-Control header. This header offers directives to browsers (and other caching agents) about how they should cache the content and when to consider it stale.

The most common directives include:

  • max-age: Specifies the number of seconds the response remains fresh.
  • no-cache: Directs caching agents to revalidate with the server before using the cached version.
  • public/private: public means that any cache, including CDNs, can store the response. private ensures the response is user-specific and only cached at the end-user level.

By setting the appropriate cache control headers, developers can steer the caching behavior of browsers and intermediaries, thereby optimizing both server load and user experience.

Rails expires_in in Controller Actions

In the Rails ecosystem, the expires_in method is our key to effortlessly managing the cache control header. Within the context of Rails actions, using expires_in sets the Cache-Control header on the HTTP response.

Take, for instance, an application like Designer Discount Club I'm currently building. The product data updates roughly once a day, and constructing the response requires complex queries and interactions with multiple services. Below, the display_cards action powers an infinite scroll list on the client. By implementing expires_in 1.hour, public: true, we essentially direct clients to retain and reuse their cached response for an hour. When users navigate back and forth, adding this cache control header reduces ~100ms round trip time from the client's perspective and diminishes request volume to our backend Redis cache by over 60%.

Designer Discount Club
class ProductsController < ApplicationController
  ...
  CLIENT_CACHE_EXPIRY_DURATION = 1.hour
  ...

  def display_cards
    # ... expensive queries and requests to backend caches to build `response`

    expires_in CLIENT_CACHE_EXPIRY_DURATION, public: true # Set the Cache-Control header

    respond_to do |format|
      format.json do
        render json: response, status: :ok, mimetype: Mime[:json]
      end

      format.protobuf do
        render plain: response.to_proto, status: :ok, mimetype: Mime[:protobuf]
      end
    end
  end
end
Developer tools screenshot

The Subtleties of expires_in

When utilizing expires_in in your controller actions, it's pivotal to understand its various options:

  • Time Duration: This determines the freshness duration. Be it 30.minutes or 1.day, ensure it matches your application's data refresh cycle.

  • Public/Private Directive: public: true indicates any cache, including CDNs, can store the response. In contrast, private: true refers to user-specific data that should only be cached at the user level.

  • Other Directives: Directives like must_revalidate can be used for nuanced cache control. With must_revalidate, once data becomes stale, it must be re-validated with the server before being reused.

And there you have it! I've deliberately left out the many other splendid backend caching techniques integrated into Rails and those used by the display_cards endpoint above; those will be the subject of another post. Before signing off, here are some plugs for my projects:

Preparing for a software engineering interview but loathe grinding LeetCode? I crafted Firecode.io precisely for that reason. Give it a whirl!

Fancy buying furniture at 20-30% designer and trade discounts without the need to hire an interior designer? Swing by Designer Discount Club!