CrackedRuby CrackedRuby

Overview

Browser caching stores copies of web resources locally in the client's browser, eliminating the need to re-fetch unchanged resources from the server on subsequent requests. When a user visits a web page, the browser downloads HTML, CSS, JavaScript, images, and other assets. Without caching, every page visit would require downloading all these resources again, wasting bandwidth and time. Browser caching solves this by storing resources locally and checking with the server whether stored versions remain valid.

The caching system operates through HTTP headers exchanged between client and server. The server includes headers in its responses that instruct the browser how to cache the resource. These headers specify caching duration, validation requirements, and whether the resource can be stored in shared caches. The browser reads these headers and stores the resource according to the instructions.

HTTP caching involves two main models: expiration and validation. Expiration-based caching uses timestamps or age limits to determine when a cached resource becomes stale. The browser serves the cached version without contacting the server until the expiration time passes. Validation-based caching stores resources but checks with the server before using them, typically sending a validator like an ETag or Last-Modified timestamp. The server responds with either a full resource or a 304 Not Modified status indicating the cached version remains current.

Browser caches exist at multiple levels. The private browser cache stores resources for a single user, while shared caches like proxy servers and CDNs serve multiple users. This distinction affects which resources can be cached where—public resources suitable for shared caching versus private resources containing user-specific data.

# Basic cache header setting in Rack application
class CachedStaticAssets
  def call(env)
    status, headers, body = @app.call(env)
    
    if env['PATH_INFO'] =~ /\.(css|js|png|jpg)$/
      headers['Cache-Control'] = 'public, max-age=31536000'
      headers['Expires'] = (Time.now + 31536000).httpdate
    end
    
    [status, headers, body]
  end
end

Understanding browser caching requires knowledge of HTTP protocol semantics, web server configuration, and client behavior across different browsers and network conditions. Proper caching implementation significantly impacts application performance, user experience, and infrastructure costs.

Key Principles

Browser caching operates through several fundamental mechanisms defined in HTTP specifications. The Cache-Control header provides the primary directives controlling cache behavior. This header accepts multiple directives that specify whether responses can be cached, who can cache them, and for how long they remain valid. The public directive allows caching in shared caches, while private restricts caching to the user's browser. The max-age directive sets a time-to-live in seconds, and no-cache forces validation with the origin server before using cached content.

Cache validation determines whether stored resources remain current. Strong validators like ETags provide a unique identifier for each version of a resource. When the browser requests a resource it has cached, it includes the ETag in an If-None-Match header. The server compares this ETag with the current version's ETag. If they match, the server responds with 304 Not Modified and no body, saving bandwidth. If they differ, the server sends the updated resource with a new ETag.

Weak validators like Last-Modified timestamps provide less precise validation. The browser sends the cached timestamp in an If-Modified-Since header. The server compares this timestamp with the resource's modification time. If unchanged, it returns 304 Not Modified. Weak validators suffice when resources change infrequently or exact byte-for-byte equality matters less than semantic equivalence.

# ETag generation and validation
require 'digest'

class ETagMiddleware
  def call(env)
    status, headers, body = @app.call(env)
    
    # Generate ETag from response body
    body_content = body.respond_to?(:join) ? body.join : body.to_s
    etag = Digest::MD5.hexdigest(body_content)
    
    headers['ETag'] = %("#{etag}")
    
    # Check If-None-Match header
    if env['HTTP_IF_NONE_MATCH'] == %("#{etag}")
      [304, {'ETag' => %("#{etag}")}, []]
    else
      [status, headers, [body_content]]
    end
  end
end

Freshness defines the period during which a cached resource can be served without validation. Explicit freshness comes from Cache-Control max-age or Expires headers. Implicit freshness calculations occur when neither is present, using heuristics based on Last-Modified age. Browsers typically cache such responses for 10% of the time since last modification, though this varies by implementation.

Staleness occurs when the freshness lifetime expires. Stale responses should not be served without validation, though stale-while-revalidate and stale-if-error directives allow serving stale content under specific circumstances. These directives improve resilience when origin servers become unavailable.

Cache storage mechanisms vary across browsers. Memory caches hold recently accessed resources in RAM for immediate retrieval. Disk caches persist resources across browser sessions. Service Workers provide programmable caching through the Cache API, allowing fine-grained control over what gets cached and when. Each storage mechanism has size limits that vary by browser and user settings.

# Implementing conditional requests with Last-Modified
class ConditionalGetMiddleware
  def call(env)
    status, headers, body = @app.call(env)
    
    # Set Last-Modified header
    last_modified = Time.now.httpdate
    headers['Last-Modified'] = last_modified
    
    # Check If-Modified-Since header
    if_modified_since = env['HTTP_IF_MODIFIED_SINCE']
    
    if if_modified_since && if_modified_since == last_modified
      [304, {'Last-Modified' => last_modified}, []]
    else
      [status, headers, body]
    end
  end
end

Vary headers control cache key generation based on request headers. When a response includes Vary: Accept-Encoding, caches maintain separate entries for gzipped and uncompressed versions. This prevents serving compressed content to clients that cannot decompress it. The Vary header significantly impacts cache efficiency—varying on many headers reduces hit rates.

Ruby Implementation

Ruby web applications control browser caching through HTTP headers set in responses. The Rack middleware layer provides the foundation for all Ruby web frameworks, making Rack the natural place to implement caching logic. Rails, Sinatra, and other frameworks build on these primitives while adding convenience methods.

Rails provides declarative cache control through controller methods. The expires_in method sets Cache-Control and Expires headers with a single call. The fresh_when method implements conditional GET support using ETags and Last-Modified timestamps. Rails automatically generates ETags from response bodies or specified attributes, handling the If-None-Match header comparison and 304 response generation.

# Rails controller with cache control
class ArticlesController < ApplicationController
  def show
    @article = Article.find(params[:id])
    
    # Set cache headers for 1 hour
    expires_in 1.hour, public: true
    
    # Conditional GET support using ETag
    fresh_when(@article)
  end
  
  def index
    @articles = Article.recent
    
    # Generate ETag from collection and modification time
    fresh_when([@articles, @articles.maximum(:updated_at)])
  end
end

Rack middleware offers lower-level control over caching behavior. Rack::Cache provides a full HTTP cache implementation that can store responses in memory, on disk, or in memcached. This middleware intercepts requests, checks for cached responses, and serves them when valid. It handles validation, expiration, and cache key generation according to HTTP specifications.

# Rack::Cache configuration
require 'rack/cache'

use Rack::Cache,
  metastore:   'file:/var/cache/rack/meta',
  entitystore: 'file:/var/cache/rack/body',
  verbose:     true

# Custom cache configuration
use Rack::Cache do
  set :verbose, true
  set :metastore, 'memcached://localhost:11211/meta'
  set :entitystore, 'memcached://localhost:11211/body'
end

Sinatra applications set cache headers directly through response headers or using helper methods. The cache_control helper provides a clean interface for setting Cache-Control directives. The last_modified and etag helpers implement conditional GET support.

# Sinatra caching implementation
require 'sinatra'

get '/article/:id' do
  @article = Article.find(params[:id])
  
  # Set cache control
  cache_control :public, :max_age => 3600
  
  # Conditional GET with ETag
  etag @article.cache_key
  
  # Conditional GET with Last-Modified
  last_modified @article.updated_at
  
  erb :article
end

get '/feed.xml' do
  @articles = Article.recent
  
  # Private cache with shorter lifetime
  cache_control :private, :max_age => 300
  
  content_type 'application/xml'
  builder :feed
end

Asset pipeline integration in Rails automatically handles static asset caching through fingerprinting. Rails generates unique filenames for CSS, JavaScript, and image files by appending content hashes. This allows aggressive caching with long expiration times since changed content receives a new filename. The asset pipeline sets appropriate cache headers automatically.

# Rails asset caching configuration
# config/environments/production.rb
Rails.application.configure do
  # Enable asset fingerprinting
  config.assets.digest = true
  
  # Set far-future expires headers
  config.static_cache_control = "public, max-age=31536000, immutable"
  
  # Configure CDN for assets
  config.asset_host = 'https://cdn.example.com'
end

# Asset helpers generate fingerprinted URLs
# <%= stylesheet_link_tag 'application' %>
# Generates: <link href="/assets/application-abc123.css" />

Custom middleware enables application-specific caching strategies. Fragment caching, Russian Doll caching, and other patterns can be implemented through middleware that analyzes requests and responses, generates cache keys, and stores rendered content.

# Custom fragment cache middleware
class FragmentCache
  def initialize(app, store)
    @app = app
    @store = store
  end
  
  def call(env)
    request = Rack::Request.new(env)
    cache_key = generate_key(request)
    
    # Check cache
    if cached = @store.read(cache_key)
      return [200, cached[:headers], [cached[:body]]]
    end
    
    # Generate response
    status, headers, body = @app.call(env)
    body_content = body.map(&:to_s).join
    
    # Store in cache if cacheable
    if cacheable?(status, headers)
      @store.write(cache_key, {
        headers: headers,
        body: body_content
      }, expires_in: extract_ttl(headers))
    end
    
    [status, headers, [body_content]]
  end
  
  private
  
  def generate_key(request)
    "#{request.request_method}:#{request.path}:#{request.query_string}"
  end
  
  def cacheable?(status, headers)
    status == 200 && headers['Cache-Control'] !~ /no-cache|no-store|private/
  end
  
  def extract_ttl(headers)
    if headers['Cache-Control'] =~ /max-age=(\d+)/
      $1.to_i
    else
      300 # Default 5 minutes
    end
  end
end

Practical Examples

Static asset caching represents the most straightforward caching scenario. Images, stylesheets, and JavaScript files rarely change, making them ideal candidates for aggressive caching. Setting a one-year max-age for these assets dramatically reduces server load and improves page load times for returning visitors.

# Static asset caching with Rack middleware
class StaticAssetCache
  ASSET_EXTENSIONS = %w[.jpg .jpeg .png .gif .svg .css .js .woff .woff2 .ttf .eot]
  
  def initialize(app)
    @app = app
  end
  
  def call(env)
    status, headers, body = @app.call(env)
    
    path = env['PATH_INFO']
    extension = File.extname(path).downcase
    
    if ASSET_EXTENSIONS.include?(extension)
      # Aggressive caching for static assets
      headers['Cache-Control'] = 'public, max-age=31536000, immutable'
      headers['Expires'] = (Time.now + 31536000).httpdate
      
      # Generate ETag from file content
      body_content = body.respond_to?(:join) ? body.join : body.to_s
      etag = Digest::MD5.hexdigest(body_content)
      headers['ETag'] = %("#{etag}")
    end
    
    [status, headers, body]
  end
end

API response caching requires more nuance. API endpoints often return data that changes frequently but not on every request. Implementing short cache lifetimes with validation ensures clients receive updated data without unnecessary network round trips.

# API response caching in Rails
class Api::ArticlesController < ApplicationController
  def index
    @articles = Article.includes(:author).recent.limit(20)
    
    # Cache for 5 minutes
    expires_in 5.minutes, public: true
    
    # Add ETag for validation
    fresh_when(@articles)
    
    render json: @articles
  end
  
  def show
    @article = Article.includes(:author, :comments).find(params[:id])
    
    # Conditional GET with both validators
    response.headers['Last-Modified'] = @article.updated_at.httpdate
    
    if stale?(etag: @article, last_modified: @article.updated_at)
      render json: @article
    end
  end
  
  def create
    @article = Article.create(article_params)
    
    # Never cache POST responses
    expires_now
    
    render json: @article, status: :created
  end
end

User-specific content caching demonstrates private cache usage. Dashboard data, user preferences, and personalized content should not be shared between users but can be cached in the individual user's browser.

# Private cache for user-specific content
class DashboardController < ApplicationController
  before_action :authenticate_user!
  
  def show
    @dashboard = Dashboard.new(current_user)
    
    # Private cache, 2 minute lifetime
    expires_in 2.minutes, public: false
    
    # ETag includes user ID to prevent cross-user caching
    cache_key = [@dashboard, current_user.id]
    fresh_when(cache_key)
  end
  
  def activity
    @activities = current_user.activities.recent
    
    # No caching for real-time activity
    response.headers['Cache-Control'] = 'no-store'
    
    render json: @activities
  end
end

Conditional request handling shows validation-based caching in action. The server generates an ETag from resource content and compares it with the client's If-None-Match header, responding with 304 when they match.

# Detailed conditional request handling
class DocumentController < ApplicationController
  def show
    @document = Document.find(params[:id])
    
    # Generate strong ETag from content and metadata
    etag_source = [@document.content, @document.updated_at, @document.version]
    etag = Digest::SHA256.hexdigest(etag_source.join(':'))
    
    response.headers['ETag'] = %("#{etag}")
    response.headers['Last-Modified'] = @document.updated_at.httpdate
    response.headers['Cache-Control'] = 'private, max-age=300'
    
    # Check both validators
    if_none_match = request.headers['If-None-Match']
    if_modified_since = request.headers['If-Modified-Since']
    
    if if_none_match == %("#{etag}")
      head :not_modified
      return
    end
    
    if if_modified_since
      client_time = Time.httpdate(if_modified_since)
      if @document.updated_at <= client_time
        head :not_modified
        return
      end
    end
    
    render json: @document
  end
end

Common Patterns

Cache-Control directives provide precise control over caching behavior. The public directive allows shared caches like CDNs to store responses, while private restricts caching to the user's browser. The max-age directive specifies freshness lifetime in seconds. The must-revalidate directive forces validation when content becomes stale rather than allowing browsers to serve stale content during network issues.

# Cache-Control directive patterns
class CacheControlPatterns
  # Pattern 1: Long-lived public assets
  def static_asset_headers
    {
      'Cache-Control' => 'public, max-age=31536000, immutable'
    }
  end
  
  # Pattern 2: Short-lived API responses
  def api_response_headers
    {
      'Cache-Control' => 'public, max-age=300, must-revalidate'
    }
  end
  
  # Pattern 3: Private user data
  def private_data_headers
    {
      'Cache-Control' => 'private, max-age=600'
    }
  end
  
  # Pattern 4: Always revalidate
  def revalidation_required_headers
    {
      'Cache-Control' => 'no-cache'
    }
  end
  
  # Pattern 5: Never cache
  def no_cache_headers
    {
      'Cache-Control' => 'no-store, no-cache, must-revalidate',
      'Pragma' => 'no-cache',
      'Expires' => '0'
    }
  end
end

Asset fingerprinting solves cache invalidation for static resources. Appending a content hash to filenames creates unique URLs for each version. Browsers cache these aggressively since changed content receives a new URL. This pattern applies to CSS, JavaScript, images, and other static files.

# Asset fingerprinting implementation
class AssetFingerprinter
  def initialize(asset_path)
    @asset_path = asset_path
  end
  
  def fingerprinted_path
    content = File.read(@asset_path)
    digest = Digest::MD5.hexdigest(content)[0..7]
    
    ext = File.extname(@asset_path)
    base = File.basename(@asset_path, ext)
    dir = File.dirname(@asset_path)
    
    "#{dir}/#{base}-#{digest}#{ext}"
  end
  
  def self.generate_manifest(asset_dir)
    manifest = {}
    
    Dir.glob("#{asset_dir}/**/*").each do |path|
      next if File.directory?(path)
      
      fingerprinter = new(path)
      original = path.sub(asset_dir, '')
      fingerprinted = fingerprinter.fingerprinted_path.sub(asset_dir, '')
      
      manifest[original] = fingerprinted
    end
    
    manifest
  end
end

Stale-while-revalidate improves resilience by serving stale content while fetching fresh content in the background. This pattern reduces perceived latency and maintains availability during origin server slowness.

# Stale-while-revalidate pattern
class StaleWhileRevalidateCache
  def initialize(app, store)
    @app = app
    @store = store
  end
  
  def call(env)
    cache_key = generate_key(env)
    cached = @store.read(cache_key)
    
    if cached
      age = Time.now - cached[:timestamp]
      
      # Fresh content - serve immediately
      if age < cached[:max_age]
        return cached[:response]
      end
      
      # Stale but within revalidation window
      if age < cached[:max_age] + cached[:stale_while_revalidate]
        # Serve stale content immediately
        Thread.new { revalidate(env, cache_key) }
        return cached[:response]
      end
    end
    
    # No cache or too stale - fetch fresh
    response = @app.call(env)
    store_response(cache_key, response, env)
    response
  end
  
  private
  
  def revalidate(env, cache_key)
    response = @app.call(env)
    store_response(cache_key, response, env)
  end
  
  def store_response(cache_key, response, env)
    status, headers, body = response
    
    if headers['Cache-Control'] =~ /max-age=(\d+)/
      max_age = $1.to_i
      swr = headers['Cache-Control'][/stale-while-revalidate=(\d+)/, 1].to_i
      
      @store.write(cache_key, {
        response: response,
        timestamp: Time.now,
        max_age: max_age,
        stale_while_revalidate: swr
      })
    end
  end
end

CDN integration patterns distribute cached content geographically. Setting appropriate Cache-Control headers ensures CDN edge servers cache content effectively. The s-maxage directive specifies cache lifetime for shared caches separately from browser caches.

# CDN cache configuration
class CDNCacheHeaders
  def initialize(app)
    @app = app
  end
  
  def call(env)
    status, headers, body = @app.call(env)
    
    path = env['PATH_INFO']
    
    case path
    when /\.(css|js|jpg|png|gif|svg|woff2?)$/
      # Static assets: Long CDN cache, long browser cache
      headers['Cache-Control'] = 'public, max-age=31536000, s-maxage=31536000, immutable'
      
    when /^\/api\//
      # API responses: Short CDN cache, validation required
      headers['Cache-Control'] = 'public, max-age=60, s-maxage=300, must-revalidate'
      
    when /^\/user\//
      # User pages: CDN cache with validation, shorter browser cache
      headers['Cache-Control'] = 'public, max-age=300, s-maxage=900, must-revalidate'
      headers['Vary'] = 'Cookie'
      
    else
      # Default: Short cache with validation
      headers['Cache-Control'] = 'public, max-age=60, must-revalidate'
    end
    
    [status, headers, body]
  end
end

Performance Considerations

Bandwidth reduction represents the most direct performance benefit. Cached resources eliminate network transfer for repeat requests. A website with 2MB of assets on first visit might transfer only 10KB on subsequent visits if assets are cached. This reduction multiplies across millions of users, significantly decreasing bandwidth costs and improving load times.

Latency improvements occur when browsers serve resources from local cache rather than requesting them over the network. Network round trips typically take 50-200ms depending on distance and connection quality. Cache hits eliminate this latency entirely. Pages with dozens of resources benefit dramatically—a page loading 30 cached resources saves 1.5-6 seconds of network time.

# Measuring cache performance
class CacheMetrics
  def initialize(app)
    @app = app
    @hits = 0
    @misses = 0
  end
  
  def call(env)
    start_time = Time.now
    
    status, headers, body = @app.call(env)
    
    duration = Time.now - start_time
    
    # Log cache hit or miss
    if status == 304
      @hits += 1
      log_metric('cache.hit', duration)
    else
      @misses += 1
      log_metric('cache.miss', duration)
    end
    
    # Add cache metrics to headers for monitoring
    headers['X-Cache-Hits'] = @hits.to_s
    headers['X-Cache-Misses'] = @misses.to_s
    headers['X-Cache-Ratio'] = (@hits.to_f / (@hits + @misses)).round(3).to_s
    
    [status, headers, body]
  end
  
  private
  
  def log_metric(name, duration)
    Rails.logger.info("#{name}: #{duration}s")
  end
end

Server load reduction follows from fewer requests reaching application servers. A well-cached application might serve 80-90% of requests from browser or CDN caches. This reduction allows fewer servers to handle the same user base, lowering infrastructure costs. CPU and memory usage decrease proportionally to the reduction in dynamic request processing.

Cache hit ratio measures caching effectiveness as the percentage of requests served from cache. A 90% hit ratio means only 10% of requests reach origin servers. Optimizing hit ratio involves balancing cache lifetime against content freshness requirements. Longer cache lifetimes increase hit ratios but risk serving stale content.

# Cache hit ratio optimization
class CacheOptimizer
  def initialize(store)
    @store = store
    @metrics = Hash.new { |h, k| h[k] = { hits: 0, misses: 0 } }
  end
  
  def record_hit(resource_type)
    @metrics[resource_type][:hits] += 1
  end
  
  def record_miss(resource_type)
    @metrics[resource_type][:misses] += 1
  end
  
  def hit_ratio(resource_type)
    metrics = @metrics[resource_type]
    total = metrics[:hits] + metrics[:misses]
    return 0 if total.zero?
    
    (metrics[:hits].to_f / total * 100).round(2)
  end
  
  def report
    @metrics.map do |type, data|
      {
        type: type,
        hits: data[:hits],
        misses: data[:misses],
        ratio: hit_ratio(type),
        recommendation: recommend_ttl(type, hit_ratio(type))
      }
    end
  end
  
  private
  
  def recommend_ttl(type, ratio)
    case ratio
    when 0..50
      "Increase cache lifetime - hit ratio too low"
    when 50..75
      "Consider increasing cache lifetime"
    when 75..90
      "Cache lifetime acceptable"
    else
      "Cache lifetime optimal"
    end
  end
end

Compression interacts with caching to amplify performance gains. Gzipped responses reduce bandwidth significantly, and caching compressed responses avoids repeated compression overhead. The Vary: Accept-Encoding header ensures browsers receive appropriately compressed content.

Network condition adaptation affects cache strategy. Mobile networks have higher latency and lower bandwidth than fixed connections. Aggressive caching benefits mobile users disproportionately. Service Workers enable sophisticated offline-first strategies that pre-cache critical resources and serve them during poor connectivity.

# Adaptive caching based on network conditions
class AdaptiveCacheControl
  def initialize(app)
    @app = app
  end
  
  def call(env)
    status, headers, body = @app.call(env)
    
    # Detect connection type from headers
    connection_type = env['HTTP_DOWNLINK'] || env['HTTP_ECT']
    save_data = env['HTTP_SAVE_DATA'] == 'on'
    
    path = env['PATH_INFO']
    
    if save_data || slow_connection?(connection_type)
      # Aggressive caching for slow connections
      if path =~ /\.(css|js)$/
        headers['Cache-Control'] = 'public, max-age=86400, immutable'
      elsif path =~ /\.(jpg|png)$/
        headers['Cache-Control'] = 'public, max-age=604800, immutable'
      end
    else
      # Standard caching for fast connections
      if path =~ /\.(css|js)$/
        headers['Cache-Control'] = 'public, max-age=3600'
      end
    end
    
    [status, headers, body]
  end
  
  private
  
  def slow_connection?(type)
    ['slow-2g', '2g', 'slow-3g'].include?(type)
  end
end

Security Implications

Sensitive data caching creates privacy and security risks. Personal information, authentication tokens, or confidential content must not be cached in shared caches. The private directive restricts caching to the user's browser, preventing CDN or proxy storage. The no-store directive prevents all caching, appropriate for highly sensitive content.

# Secure caching for sensitive data
class SecureCacheControl
  def initialize(app)
    @app = app
  end
  
  def call(env)
    status, headers, body = @app.call(env)
    
    path = env['PATH_INFO']
    
    case path
    when /^\/api\/user\/profile/
      # Personal data - private cache only
      headers['Cache-Control'] = 'private, max-age=300'
      headers['Vary'] = 'Authorization'
      
    when /^\/api\/admin/
      # Admin data - no caching
      headers['Cache-Control'] = 'no-store, no-cache, must-revalidate'
      headers['Pragma'] = 'no-cache'
      
    when /^\/api\/auth/
      # Authentication endpoints - never cache
      headers['Cache-Control'] = 'no-store'
      
    when /^\/api\/payment/
      # Payment data - no caching
      headers['Cache-Control'] = 'no-store, no-cache'
      headers['Clear-Site-Data'] = '"cache"'
    end
    
    [status, headers, body]
  end
end

Cache poisoning attacks inject malicious content into shared caches. If an attacker can control cache keys or response content, they might store malicious responses that serve to other users. Defense requires validating cache key inputs, sanitizing response content, and using secure cache key generation algorithms.

HTTPS and caching interaction requires careful consideration. Browsers often refuse to cache HTTPS responses without explicit caching headers. The Cache-Control header must be present even when defaults would cache HTTP responses. Mixed content warnings occur when HTTPS pages include cached HTTP resources.

# HTTPS-aware caching
class HTTPSCacheMiddleware
  def initialize(app)
    @app = app
  end
  
  def call(env)
    status, headers, body = @app.call(env)
    
    if env['HTTPS'] == 'on' || env['HTTP_X_FORWARDED_PROTO'] == 'https'
      # HTTPS requires explicit cache headers
      if headers['Cache-Control'].nil?
        headers['Cache-Control'] = 'private, max-age=0, must-revalidate'
      end
      
      # Prevent mixed content
      headers['Content-Security-Policy'] ||= "upgrade-insecure-requests"
      
      # HSTS header prevents HTTPS downgrade
      headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains'
    end
    
    [status, headers, body]
  end
end

Authentication and caching present challenges. Authenticated API responses contain user-specific data but might benefit from private caching. The Vary: Authorization header ensures different authentication credentials produce separate cache entries. Session cookies require similar treatment through Vary: Cookie.

# Authentication-aware caching
class AuthCacheMiddleware
  def initialize(app)
    @app = app
  end
  
  def call(env)
    status, headers, body = @app.call(env)
    
    auth_header = env['HTTP_AUTHORIZATION']
    cookie_header = env['HTTP_COOKIE']
    
    if auth_header || cookie_header
      # Authenticated request - private cache with validation
      headers['Cache-Control'] = 'private, max-age=60, must-revalidate'
      
      # Vary on authentication to prevent cross-user caching
      vary_headers = []
      vary_headers << 'Authorization' if auth_header
      vary_headers << 'Cookie' if cookie_header
      
      headers['Vary'] = vary_headers.join(', ')
      
      # Add cache tags for invalidation
      if user_id = extract_user_id(auth_header, cookie_header)
        headers['Cache-Tag'] = "user:#{user_id}"
      end
    end
    
    [status, headers, body]
  end
  
  private
  
  def extract_user_id(auth_header, cookie_header)
    # Extract user ID from authentication
    # Implementation depends on auth mechanism
  end
end

Common Pitfalls

Over-caching dynamic content causes users to see outdated information. Setting long cache lifetimes for frequently updated content creates staleness problems. A news site caching articles for hours shows readers old headlines. Finding the right balance between performance and freshness requires understanding content update frequency and user expectations.

# Dynamic content cache management
class DynamicContentCache
  CACHE_RULES = {
    '/articles/latest' => { max_age: 60, must_revalidate: true },
    '/articles/popular' => { max_age: 300, stale_while_revalidate: 60 },
    '/articles/archive' => { max_age: 3600, immutable: false },
    '/api/realtime' => { no_cache: true }
  }
  
  def cache_headers(path)
    rule = CACHE_RULES[path] || default_rule
    
    directives = []
    directives << 'public'
    
    if rule[:no_cache]
      return 'no-store, no-cache, must-revalidate'
    end
    
    directives << "max-age=#{rule[:max_age]}" if rule[:max_age]
    directives << 'must-revalidate' if rule[:must_revalidate]
    directives << "stale-while-revalidate=#{rule[:stale_while_revalidate]}" if rule[:stale_while_revalidate]
    directives << 'immutable' if rule[:immutable]
    
    directives.join(', ')
  end
  
  def default_rule
    { max_age: 300, must_revalidate: true }
  end
end

Cache invalidation complexity increases with distributed caches. When content changes, all cached copies must be invalidated or revalidated. CDN edge servers, browser caches, and intermediate proxies might all hold stale versions. Surrogate-Control headers and cache purge APIs address distributed invalidation, but coordination remains challenging.

Inconsistent cache headers across different resources cause unpredictable behavior. An HTML page cached for 1 hour that references a CSS file cached for 1 day creates version mismatches. Users might see old styles with new markup. Coordinating cache lifetimes across dependent resources requires careful planning.

# Coordinated cache management for dependent resources
class DependentResourceCache
  def initialize(app)
    @app = app
    @version = load_asset_version
  end
  
  def call(env)
    status, headers, body = @app.call(env)
    
    path = env['PATH_INFO']
    
    # HTML pages reference versioned assets
    if path =~ /\.html$/ || path == '/'
      headers['Cache-Control'] = 'public, max-age=300, must-revalidate'
      headers['X-Asset-Version'] = @version
      
    # Assets include version in cache headers
    elsif path =~ /\.(css|js)$/
      headers['Cache-Control'] = 'public, max-age=31536000, immutable'
      headers['X-Asset-Version'] = @version
      
      # Verify version matches
      if env['HTTP_X_ASSET_VERSION'] && env['HTTP_X_ASSET_VERSION'] != @version
        # Version mismatch - force refresh
        headers['Cache-Control'] = 'no-cache'
      end
    end
    
    [status, headers, body]
  end
  
  private
  
  def load_asset_version
    File.read('ASSET_VERSION').strip
  rescue
    Time.now.to_i.to_s
  end
end

Mobile and desktop cache behavior differs. Mobile browsers often have smaller cache sizes and more aggressive cache eviction. Service Workers enable sophisticated mobile caching strategies but require additional code. Testing cache behavior across devices reveals inconsistencies that desktop-only testing misses.

Debugging cache issues proves difficult. Browsers cache responses silently, making it unclear whether content comes from cache or server. Response headers might be cached incorrectly. Browser developer tools show cache status, but reproducing issues requires understanding cache state. Hard refresh bypasses cache, masking problems that regular users encounter.

# Cache debugging helper
class CacheDebugger
  def initialize(app)
    @app = app
  end
  
  def call(env)
    status, headers, body = @app.call(env)
    
    # Add debug headers in development
    if ENV['RACK_ENV'] == 'development'
      headers['X-Cache-Debug'] = cache_debug_info(env, headers)
    end
    
    # Log cache information
    log_cache_request(env, status, headers)
    
    [status, headers, body]
  end
  
  private
  
  def cache_debug_info(env, headers)
    info = []
    info << "path:#{env['PATH_INFO']}"
    info << "control:#{headers['Cache-Control']}"
    info << "etag:#{headers['ETag']}"
    info << "modified:#{headers['Last-Modified']}"
    info << "if-none-match:#{env['HTTP_IF_NONE_MATCH']}"
    info << "if-modified-since:#{env['HTTP_IF_MODIFIED_SINCE']}"
    info.join(' | ')
  end
  
  def log_cache_request(env, status, headers)
    Rails.logger.info({
      path: env['PATH_INFO'],
      method: env['REQUEST_METHOD'],
      status: status,
      cache_control: headers['Cache-Control'],
      etag_sent: headers['ETag'],
      etag_received: env['HTTP_IF_NONE_MATCH'],
      modified_sent: headers['Last-Modified'],
      modified_received: env['HTTP_IF_MODIFIED_SINCE']
    }.to_json)
  end
end

Reference

Cache-Control Directives

Directive Scope Description
public Response Allows caching in shared caches
private Response Restricts caching to browser only
no-cache Request/Response Must revalidate before using cached response
no-store Request/Response Must not store response in any cache
max-age Response Seconds until response becomes stale
s-maxage Response Max age for shared caches, overrides max-age
must-revalidate Response Must revalidate once stale, no stale serving
proxy-revalidate Response Like must-revalidate but only for shared caches
no-transform Response Intermediaries must not transform content
immutable Response Indicates content will never change
stale-while-revalidate Response Seconds to serve stale while revalidating
stale-if-error Response Seconds to serve stale if origin errors

HTTP Headers Reference

Header Direction Purpose
Cache-Control Response Primary cache control mechanism
Expires Response HTTP/1.0 expiration date, superseded by max-age
ETag Response Unique identifier for resource version
Last-Modified Response Timestamp of last modification
If-None-Match Request ETag value for conditional request
If-Modified-Since Request Timestamp for conditional request
Vary Response Headers to include in cache key
Age Response Seconds response has been in cache
Pragma Request/Response HTTP/1.0 cache control, use Cache-Control instead

Status Codes

Code Name Description
200 OK Full response with content
304 Not Modified Cached version still valid
412 Precondition Failed Conditional request precondition not met

Rails Cache Methods

Method Purpose Example
expires_in Set cache duration expires_in 1.hour, public: true
expires_now Disable caching expires_now
fresh_when Conditional GET with ETag fresh_when(@article)
stale? Check if cached version stale if stale?(@article)
http_cache_forever Permanent caching http_cache_forever(public: true)

Rack::Cache Configuration

Option Type Purpose
metastore String Location for cache metadata
entitystore String Location for cached responses
verbose Boolean Enable detailed logging
allow_reload Boolean Honor Cache-Control: max-age=0
allow_revalidate Boolean Honor Cache-Control: no-cache

Asset Cache Configuration

Setting Rails Config Purpose
Digest config.assets.digest Enable fingerprinting
Static Cache Control config.static_cache_control Headers for static files
Asset Host config.asset_host CDN URL for assets
Compile config.assets.compile Runtime compilation

Common Cache Lifetime Values

Content Type Recommended Duration Cache-Control Example
Static assets with fingerprinting 1 year public, max-age=31536000, immutable
Static assets without fingerprinting 1 day public, max-age=86400
API responses 5 minutes public, max-age=300, must-revalidate
HTML pages 5 minutes public, max-age=300
User-specific data 2 minutes private, max-age=120
Real-time data No cache no-store, no-cache
Authentication responses No cache no-store