Harnessing Client-Side Caching in Rails: The Power of `expires_in`
Tags: webdev, tutorial, rails, softwareengineering
Published: 2023-10-05 12:17:26

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](https://designerdiscount.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 Page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/xqa9gpllohnuq2iqrll8.png)

```ruby
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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ef6qlq10cie4vqa7gjet.png)

## 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](https://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](https://designerdiscount.club)!