This document describes Folio's cache management system, including HTTP cache headers, fragment caching, and development tools integration.
Folio provides a caching system with:
- HTTP Cache Headers - Smart browser and CDN caching (opt-in via config)
- Fragment Caching - View-level caching for performance
- Development Integration - MiniProfiler management
- Flexible Configuration - ENV-based overrides for development
Important: Cache headers are disabled by default in Folio Engine. You must explicitly enable them in your application.
Folio after enabling automatically sets appropriate Cache-Control headers based on content type and publishing status:
# Published content - cacheable
Cache-Control: max-age=60, must-revalidate, stale-while-revalidate=15, stale-if-error=300
# Unpublished content - not cacheable
Cache-Control: no-store
# 404 error pages - same TTL as regular pages (attack prevention)
Cache-Control: max-age=60, must-revalidate
# Server errors (500+) - shorter TTL
Cache-Control: max-age=15, must-revalidate
# Admin/console paths - never cached
Cache-Control: private, no-store
# Signed-in users - private cache
Cache-Control: private, max-age=60Cache headers are configured in config/initializers/cache_headers.rb with ENV variable support:
# Core settings
config.folio_cache_headers_enabled = true # MASTER SWITCH - enables entire system
config.folio_cache_headers_default_ttl = 60 # Default TTL in seconds
# Header inclusion
config.folio_cache_headers_include_etag = true # Include ETag headers
config.folio_cache_headers_include_last_modified = true # Include Last-Modified headers
config.folio_cache_headers_include_cache_tags = false # Include cache tags
# Advanced settings
config.folio_cache_headers_stale_while_revalidate = 15 # Stale-while-revalidate seconds
config.folio_cache_headers_stale_if_error = 300 # Stale-if-error secondsAll settings can be overridden via ENV variables (see Development Environment Controls below).
Cache headers are disabled by default in Folio Engine. To enable them in your application:
Generate the cache headers initializer:
rails generate folio:cache_headersThis creates config/initializers/cache_headers.rb with cache headers enabled.
Add to your config/initializers/cache_headers.rb:
# Enable cache headers system
Rails.application.config.folio_cache_headers_enabled = true
# Configure other settings as needed
Rails.application.config.folio_cache_headers_default_ttl = 60For testing or temporary enabling:
FOLIO_CACHE_HEADERS_ENABLED=true rails serverIn your controllers, use set_cache_control_headers:
class ArticlesController < ApplicationController
def show
@article = Article.find(params[:id])
set_cache_control_headers(record: @article)
end
endIf you need to set custom cache headers that differ from the automatic system, you can call the cache header methods directly in your action. The automatic after_action callback will detect that headers are already set and skip:
class Api::ThumbnailsController < Api::BaseController
def show
cache_ttl = calculate_custom_ttl
# Set public cache headers early in the action
set_public_cache_headers(cache_ttl)
# ... rest of action logic ...
end
endHow it works:
- Setting headers early in the action ensures they're in place before the
after_actioncallback runs - The automatic cache headers system checks if
Cache-Controlis already set (line 30 inFolio::HttpCache::Headers) - If headers are present, it skips and logs:
"cache_control_already_set" - This prevents the automatic system from adding
privatefor signed-in users or other automatic behavior
Available methods:
set_public_cache_headers(ttl)- Sets public cache headers with Vary headersset_private_cache_headers_with_ttl(ttl)- Sets private cache headers with TTLset_private_cache_headers- Setsno-cacheheaders
Folio provides specific patterns for action and view caching:
# In controllers - cache entire action logic
folio_run_unless_cached(["blog/articles#show", params[:id]] + cache_key_base) do
@article = Article.find(params[:id])
set_meta_variables(@article)
end/ In views - cache with computed base key
- cache @cache_key
.content
= render @articleBase Cache Key: Controllers include Folio::CacheMethods and implement cache_key_base returning an array with user state, site context, and other cache-busting factors:
def cache_key_base
[request.host, user_signed_in? ? "logged_in" : "logged_out", I18n.locale]
endEnable fragment caching in development:
# Enable caching (creates tmp/caching-dev.txt)
rails dev:cache
# Disable caching (removes tmp/caching-dev.txt)
rails dev:cacheSkip caching for debugging:
http://localhost:3000/articles/123?skip_global_cache=1
When cache optimization is active (FOLIO_CACHE_SKIP_SESSION=true), components that need session state (forms, interactive elements) can declare their requirements using the polymorfic ComponentSessionHelper concern. This automatically disables session skipping when needed.
The system uses polymorphic override pattern - components that need session include a concern and declare their requirements, which are automatically detected by the cache headers system.
Components that need session state include the helper concern and override the session_requirement_reason method:
class MyFormComponent < ApplicationComponent
include Folio::ComponentSessionHelper
def session_requirement_reason
"contact_form_csrf"
end
endThe HTTP cache headers system automatically detects components with session requirements:
# In Folio::HttpCache::Headers
def should_skip_session_for_cache?
# Global configuration check first
unless Rails.application.config.folio_cache_skip_session_for_public
return false
end
# Automatic detection of ComponentSessionRequirements concern
# No manual class name checking needed!
super if defined?(super)
endControllers automatically get the concern via ApplicationControllerBase:
# Automatically included in all controllers
include Folio::ComponentSessionRequirements
# Provides polymorphic override
def should_skip_session_for_cache?
# Auto-analyze @page if exists
if defined?(@page) && @page&.respond_to?(:atoms)
analyze_page_session_requirements(@page)
end
# Check if any component requires session
return false if component_requires_session?
# Delegate to parent (Headers concern)
super if defined?(super)
end- Components declare their session needs via
session_requirement_reasonmethod - Headers concern automatically analyzes page atoms for session requirements
- Polymorphic override in ComponentSessionRequirements prevents session skipping if needed
- Cache optimization is disabled automatically when forms are present
- Result: Forms work correctly on cached pages with clean architecture
See Component Session Requirements Documentation for detailed implementation guide.
Folio automatically manages MiniProfiler to prevent interference with cache testing:
- Cache ENABLED → MiniProfiler AUTO-DISABLED
- Cache DISABLED → MiniProfiler AUTO-ENABLED
This prevents MiniProfiler from overriding cache headers during development testing.
Force MiniProfiler behavior via environment variables:
# Force enable MiniProfiler (may interfere with cache testing)
MINI_PROFILER_FORCE_ENABLED=true rails server
# Force disable MiniProfiler
MINI_PROFILER_ENABLED=false rails serverControl cache headers in development via ENV variables:
# Enable cache headers in development
FOLIO_CACHE_HEADERS_ENABLED=true rails server
# Disable cache headers (default in development)
FOLIO_CACHE_HEADERS_ENABLED=false rails server
# Custom TTL
FOLIO_CACHE_HEADERS_TTL=120 rails server
# Disable ETag headers
FOLIO_CACHE_HEADERS_ETAG=false rails server
# Custom stale-while-revalidate
FOLIO_CACHE_HEADERS_SWR=30 rails serverControl cache behavior via emergency ENV variables:
# Disable all caching immediately (sets Cache-Control: no-store)
FOLIO_CACHE_TTL_MULTIPLIER=0 rails server
# Reduce cache TTL by half
FOLIO_CACHE_TTL_MULTIPLIER=0.5 rails server
# Double cache TTL
FOLIO_CACHE_TTL_MULTIPLIER=2 rails serverWhen starting the development server, Folio displays helpful banners:
✅ FOLIO DEVELOPMENT CACHE: ENABLED
Store: Memory Store
Fragment cache logging: ON
Public file headers: 172800s TTL
MiniProfiler: Auto-disabled (prevents interference)
To disable: rails dev:cache
Documentation: docs/cache.md
❌ FOLIO DEVELOPMENT CACHE: DISABLED
Store: :null_store (no caching)
Fragment caching is OFF
Cache headers are OFF
MiniProfiler: Auto-enabled
To enable: rails dev:cache
This will create tmp/caching-dev.txt
Documentation: docs/cache.md
Cloudflare automatically sets cf-cache-status: BYPASS for responses with Set-Cookie headers, preventing CDN caching. Rails applications commonly send:
- Session cookies (
_session_id) - Rails default behavior - Log cookies (
s_for_log,u_for_log) - Folio tracking cookies
Enable cache-friendly mode that skips session cookies for anonymous users:
# Enable in production for Cloudflare optimization
FOLIO_CACHE_SKIP_SESSION=true rails serverWhat it does:
- Skips Rails session cookies for public cache responses
- Skips Folio log cookies for anonymous users
- Allows Cloudflare to cache instead of BYPASS
- Maintains cookies for signed-in users (private cache)
- Note: Business-specific cookies (like UTM tracking) maintain their own logic
Safety:
- ✅ Safe for anonymous content that doesn't need session state
- ✅ CSRF protection still works via meta tags
- ✅ User authentication unaffected
- ✅ Flash messages work normally for signed-in users
# config/initializers/cache_headers.rb
Rails.application.config.folio_cache_skip_session_for_public = true
# or via ENV
ENV["FOLIO_CACHE_SKIP_SESSION"] = "true"# Without session skip (will get BYPASS)
curl -I https://yoursite.com/ | grep -E "set-cookie|cf-cache"
# With session skip enabled
FOLIO_CACHE_SKIP_SESSION=true rails server
curl -I https://yoursite.com/ | grep -E "set-cookie|cf-cache"
# Should see no set-cookie headers for anonymous requestsFolio automatically handles session requirements for form controllers:
# Folio::LeadsController - automatically requires session for form submissions
class Folio::LeadsController < Folio::ApplicationController
include Folio::RequiresSession
requires_session_for :form_functionality, only: [:create]
end
# Folio::Api::NewsletterSubscriptionsController - session for newsletter signups
class Folio::Api::NewsletterSubscriptionsController < Folio::Api::BaseController
include Folio::RequiresSession
requires_session_for :newsletter_subscription, only: [:create]
endForm components automatically declare session requirements using the polymorphic pattern:
# Lead forms require session for CSRF and flash messages
class Folio::Leads::FormComponent < ApplicationComponent
include Folio::ComponentSessionHelper
def initialize(lead: nil)
@lead = lead || Folio::Lead.new
end
def session_requirement_reason
"lead_form_csrf_and_flash"
end
end
# Newsletter forms require session for CSRF and Turnstile
class Folio::NewsletterSubscriptions::FormComponent < ApplicationComponent
include Folio::ComponentSessionHelper
def initialize(newsletter_subscription: nil, view_options: {})
@newsletter_subscription = newsletter_subscription || Folio::NewsletterSubscription.new
@view_options = view_options
end
def session_requirement_reason
"newsletter_subscription_csrf_and_turnstile"
end
end-
Enable cache headers:
FOLIO_CACHE_HEADERS_ENABLED=true rails server
-
Check headers in browser DevTools or curl:
curl -I http://localhost:3000/some-page
-
Notice MiniProfiler is automatically disabled
-
Enable Rails caching:
rails dev:cache
-
Check logs for cache hits/misses:
Cache read: views/articles/1-20231201123456/article Cache write: views/articles/1-20231201123456/article -
Notice MiniProfiler is automatically disabled
-
Enable detailed logging:
FOLIO_CACHE_HEADERS_ENABLED=true rails server
-
Check logs for cache decisions:
[Cache Headers] ArticlesController -> public (get_request_signed_out_2xx_with_record) Headers: Cache-Control: max-age=60, must-revalidate, stale-while-revalidate=15
# If you need MiniProfiler while testing cache
MINI_PROFILER_FORCE_ENABLED=true FOLIO_CACHE_HEADERS_ENABLED=true rails server- Test with cache enabled - Periodically enable caching to catch issues early
- Check logs - Cache decision logging helps debug issues
- Use ENV overrides - Quickly test different cache configurations
- Enable cache headers - They're enabled by default in production
- Monitor cache hit rates - Use CDN analytics to track effectiveness
- Test emergency overrides - Ensure
FOLIO_CACHE_TTL_MULTIPLIER=0works - Validate ETag/Last-Modified - Check that conditional requests work
- Use
set_cache_control_headers(record: @model)- Automatic unpublished detection - Test preview mode - Ensure unpublished content isn't cached
- Consider cache invalidation - Plan for content update workflows
- Monitor stale content - Use
stale-while-revalidateappropriately
-
Check if cache headers are enabled:
Rails.application.config.folio_cache_headers_enabled
-
Verify the path should be cached:
# Console/admin paths are never cached request.path.starts_with?("/console") # Should be false
-
Check for existing headers:
# Headers set earlier take precedence response.headers["Cache-Control"] # Should be nil before setting
-
Check if auto-disabled:
[MiniProfiler] 🔄 Auto-disabled (caching active) -
Force enable if needed:
FORCE_MINI_PROFILER=true rails server
-
Verify in browser that cache headers are correct (MiniProfiler may add its own)
-
Ensure caching is enabled:
rails dev:cache
-
Check for cache store:
Rails.cache.class # Should not be NullStore
-
Verify cache key generation:
<% cache @article do %> <!-- This generates a key like: views/articles/1-20231201123456/article --> <% end %>
- Folio HTTP Cache Headers - Implementation details
- Rails Caching Guide - Rails caching basics
- MiniProfiler - Profiler documentation