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%.
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
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
or1.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. Withmust_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!