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: 
publicmeans that any cache, including CDNs, can store the response.privateensures 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.minutesor1.day, ensure it matches your application's data refresh cycle.Public/Private Directive:
public: trueindicates any cache, including CDNs, can store the response. In contrast,private: truerefers to user-specific data that should only be cached at the user level.Other Directives: Directives like
must_revalidatecan 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!
              

    
Top comments (0)