Overview
Content Delivery Networks (CDNs) consist of distributed servers that cache and deliver static content from locations geographically closer to end users. CDNs reduce latency by serving assets from edge servers near the request origin rather than requiring every request to travel to the origin server. This architecture significantly improves page load times, reduces bandwidth costs, and increases application availability.
A CDN operates by maintaining copies of static content—images, stylesheets, JavaScript files, videos—across multiple Points of Presence (PoPs) worldwide. When a user requests a resource, the CDN routes the request to the nearest edge server. If the edge server has a cached copy, it serves the content directly. If not, the edge server retrieves the content from the origin server, caches it for future requests, and delivers it to the user.
Modern web applications rely heavily on CDNs to deliver assets efficiently. A typical web page loads dozens of resources including images, stylesheets, JavaScript bundles, and fonts. Without a CDN, each resource request travels to the origin server, creating latency proportional to geographic distance. With a CDN, most requests terminate at nearby edge servers, reducing latency from hundreds of milliseconds to tens of milliseconds.
# Rails application serving assets directly (without CDN)
# config/environments/production.rb
config.asset_host = 'https://example.com'
# Assets served from: https://example.com/assets/application-abc123.css
# Rails application using CDN
config.asset_host = 'https://d3fgh82klm.cloudfront.net'
# Assets served from: https://d3fgh82klm.cloudfront.net/assets/application-abc123.css
CDNs also provide additional capabilities beyond simple caching: DDoS protection, SSL/TLS termination, image optimization, edge computing, and real-time analytics. These features transform CDNs from simple caching layers into comprehensive content delivery and security platforms.
Key Principles
CDNs operate on several foundational principles that determine their effectiveness and behavior. Understanding these principles allows developers to configure CDNs appropriately and troubleshoot performance issues.
Geographic Distribution forms the core of CDN architecture. CDN providers maintain PoPs in major cities and internet exchange points worldwide. The number and location of PoPs directly affects CDN performance—more PoPs in diverse locations provide better geographic coverage. When a user requests content, DNS resolution or anycast routing directs the request to the optimal edge server based on factors including geographic proximity, server load, and network conditions.
Cache Hit Ratio measures CDN effectiveness. A cache hit occurs when an edge server fulfills a request from its cache without contacting the origin server. A cache miss requires the edge server to fetch content from the origin. High cache hit ratios (90-95%+) indicate effective caching, while low ratios suggest configuration problems or uncacheable content. Cache hit ratio directly impacts both performance and bandwidth costs—cache hits are fast and free, while cache misses add latency and origin bandwidth consumption.
Time To Live (TTL) controls how long edge servers cache content before revalidating with the origin. CDNs determine TTL from multiple sources: explicit cache headers (Cache-Control, Expires), CDN configuration rules, and default settings. Longer TTLs improve cache hit ratios but increase the risk of serving stale content. Shorter TTLs ensure freshness but increase cache misses and origin load. Different content types require different TTL strategies—static assets with fingerprinted names can use maximum TTLs (1 year), while dynamic content requires shorter TTLs or no caching.
# Rails asset pipeline generates fingerprinted filenames
# Original: application.css
# Compiled: application-8f3d9e2b1c4a5f6e7d8c9b0a1e2f3d4c.css
# Long TTL for fingerprinted assets (1 year)
Cache-Control: public, max-age=31536000, immutable
# Short TTL for HTML pages (5 minutes)
Cache-Control: public, max-age=300
Cache Invalidation removes cached content before its TTL expires. CDN providers support several invalidation mechanisms including purging specific files, purging by tag or pattern, and versioning. Invalidation becomes necessary when content changes—deploying code updates, correcting errors, or updating dynamic content. However, invalidation has limitations: it may not propagate instantly to all edge servers, and excessive invalidation undermines caching benefits.
Origin Shield adds an intermediate caching layer between edge servers and the origin. When enabled, edge cache misses contact the shield cache instead of the origin directly. The shield cache collapses concurrent requests for the same resource, reducing origin load. This architecture benefits origins with limited capacity or expensive computation but adds a cache layer that might increase latency for cache misses.
Edge Computing executes code at CDN edge servers rather than the origin. Edge functions transform requests and responses, enabling dynamic behavior without origin involvement. Examples include resizing images on demand, redirecting users based on location, A/B testing, and authentication. Edge computing reduces origin load and latency for dynamic operations.
Ruby Implementation
Ruby web applications integrate with CDNs primarily through the Rails asset pipeline and cloud storage services. The asset pipeline compiles and fingerprints assets, while CDN integration configures where browsers load these assets.
Rails provides built-in CDN support through the asset_host configuration. Setting asset_host changes asset URLs in views to point to the CDN hostname instead of the application domain. The asset pipeline already generates fingerprinted asset names, making them ideal for long-term CDN caching.
# config/environments/production.rb
Rails.application.configure do
# Configure CDN hostname for assets
config.asset_host = 'https://cdn.example.com'
# Enable asset compilation and digesting
config.assets.compile = false
config.assets.digest = true
# Configure public file server for non-asset static files
config.public_file_server.enabled = true
config.public_file_server.headers = {
'Cache-Control' => 'public, max-age=31536000'
}
end
The asset_host configuration accepts strings, procs, or objects responding to call. Using a proc enables domain sharding—spreading assets across multiple subdomains to bypass browser connection limits:
# Domain sharding across 4 subdomains
config.asset_host = Proc.new { |source|
"https://cdn#{source.hash % 4}.example.com"
}
# Generated URLs:
# <img src="https://cdn2.example.com/assets/logo-abc123.png">
# <link href="https://cdn0.example.com/assets/application-def456.css">
For applications storing assets in cloud storage like S3, the asset_sync gem synchronizes compiled assets after deployment. This gem uploads assets to S3 during the deployment process, where CloudFront or another CDN serves them:
# Gemfile
gem 'asset_sync'
# config/initializers/asset_sync.rb
AssetSync.configure do |config|
config.fog_provider = 'AWS'
config.aws_access_key_id = ENV['AWS_ACCESS_KEY_ID']
config.aws_secret_access_key = ENV['AWS_SECRET_ACCESS_KEY']
config.fog_directory = ENV['AWS_BUCKET']
config.fog_region = 'us-east-1'
# Only sync in production
config.enabled = Rails.env.production?
# Delete old assets not in manifest
config.existing_remote_files = 'delete'
# Gzip assets before uploading
config.gzip_compression = true
end
# After deployment
# rake assets:precompile
# Assets automatically sync to S3
Direct CDN API integration uses provider-specific Ruby SDKs. The AWS SDK enables CloudFront distribution management, cache invalidation, and signed URL generation:
require 'aws-sdk-cloudfront'
class CdnManager
def initialize
@client = Aws::CloudFront::Client.new(
region: 'us-east-1',
credentials: Aws::Credentials.new(
ENV['AWS_ACCESS_KEY_ID'],
ENV['AWS_SECRET_ACCESS_KEY']
)
)
@distribution_id = ENV['CLOUDFRONT_DISTRIBUTION_ID']
end
def invalidate_paths(paths)
@client.create_invalidation({
distribution_id: @distribution_id,
invalidation_batch: {
paths: {
quantity: paths.size,
items: paths
},
caller_reference: Time.now.to_i.to_s
}
})
end
def generate_signed_url(path, expires_in: 3600)
signer = Aws::CloudFront::UrlSigner.new(
key_pair_id: ENV['CLOUDFRONT_KEY_PAIR_ID'],
private_key: ENV['CLOUDFRONT_PRIVATE_KEY']
)
signer.signed_url(
"https://#{ENV['CLOUDFRONT_DOMAIN']}/#{path}",
expires: Time.now + expires_in
)
end
end
# Usage
cdn = CdnManager.new
# Invalidate specific files after deployment
cdn.invalidate_paths(['/assets/*', '/index.html'])
# Generate temporary signed URL for private content
url = cdn.generate_signed_url('private/video.mp4', expires_in: 1.hour)
# => "https://d111111abcdef8.cloudfront.net/private/video.mp4?Expires=...&Signature=...&Key-Pair-Id=..."
The Cloudflare gem provides similar functionality for Cloudflare CDN:
require 'cloudflare'
class CloudflareManager
def initialize
@client = Cloudflare.connect(
key: ENV['CLOUDFLARE_API_KEY'],
email: ENV['CLOUDFLARE_EMAIL']
)
@zone = @client.zones.find_by_name(ENV['DOMAIN'])
end
def purge_cache(files: nil, tags: nil)
if files
@zone.purge_cache(files: files)
elsif tags
@zone.purge_cache(tags: tags)
else
@zone.purge_cache(purge_everything: true)
end
end
def update_cache_settings(level:)
@zone.settings.update(
cache_level: level # 'aggressive', 'basic', 'simplified'
)
end
end
# Usage
cf = CloudflareManager.new
# Purge specific files
cf.purge_cache(files: [
'https://example.com/assets/application.css',
'https://example.com/assets/application.js'
])
# Purge by cache tags
cf.purge_cache(tags: ['v2.0', 'homepage'])
# Purge entire cache
cf.purge_cache
Practical Examples
CloudFront with S3 Origin for Rails Assets demonstrates the most common CDN setup for Rails applications. This configuration stores precompiled assets in S3 and serves them through CloudFront, combining reliable storage with global distribution.
First, create an S3 bucket and configure it for website hosting:
# lib/tasks/cdn_setup.rake
namespace :cdn do
desc "Setup S3 bucket for CDN assets"
task setup_s3: :environment do
require 'aws-sdk-s3'
s3 = Aws::S3::Client.new(region: 'us-east-1')
bucket = ENV['ASSETS_BUCKET']
# Create bucket
s3.create_bucket(bucket: bucket)
# Configure CORS for cross-origin requests
s3.put_bucket_cors({
bucket: bucket,
cors_configuration: {
cors_rules: [{
allowed_headers: ['*'],
allowed_methods: ['GET', 'HEAD'],
allowed_origins: ['*'],
max_age_seconds: 3600
}]
}
})
# Set bucket policy for public read access
s3.put_bucket_policy({
bucket: bucket,
policy: {
Version: '2012-10-17',
Statement: [{
Sid: 'PublicReadGetObject',
Effect: 'Allow',
Principal: '*',
Action: 's3:GetObject',
Resource: "arn:aws:s3:::#{bucket}/*"
}]
}.to_json
})
end
end
Configure Rails to use the CDN and sync assets after precompilation:
# config/environments/production.rb
Rails.application.configure do
config.asset_host = "https://#{ENV['CLOUDFRONT_DOMAIN']}"
config.action_controller.asset_host = "https://#{ENV['CLOUDFRONT_DOMAIN']}"
end
# config/initializers/asset_sync.rb
AssetSync.configure do |config|
config.fog_provider = 'AWS'
config.fog_directory = ENV['ASSETS_BUCKET']
config.fog_region = 'us-east-1'
config.aws_access_key_id = ENV['AWS_ACCESS_KEY_ID']
config.aws_secret_access_key = ENV['AWS_SECRET_ACCESS_KEY']
config.gzip_compression = true
config.manifest = true
config.existing_remote_files = 'delete'
# Set far-future cache headers
config.cache_asset_regexps = [/.*/]
config.default_cache_control = 'public, max-age=31536000, immutable'
end
Dynamic Content Caching with Edge Side Includes shows how to cache page fragments while keeping user-specific content dynamic. This approach caches the page shell at the CDN while fetching personalized content from the origin.
# app/controllers/concerns/cdn_caching.rb
module CdnCaching
extend ActiveSupport::Concern
def set_cdn_cache(duration, tags: [])
response.headers['Cache-Control'] = "public, max-age=#{duration}, s-maxage=#{duration}"
response.headers['Surrogate-Key'] = tags.join(' ') if tags.any?
end
def set_cdn_vary(*headers)
response.headers['Vary'] = headers.join(', ')
end
def bypass_cdn_cache
response.headers['Cache-Control'] = 'private, no-cache, no-store'
end
end
# app/controllers/products_controller.rb
class ProductsController < ApplicationController
include CdnCaching
def show
@product = Product.find(params[:id])
# Cache product page for 1 hour, tag for invalidation
set_cdn_cache(1.hour, tags: [
"product-#{@product.id}",
"category-#{@product.category_id}"
])
# Vary cache by Accept-Encoding for compression
set_cdn_vary('Accept-Encoding')
end
def user_specific
@recommendations = current_user.recommended_products
# Don't cache user-specific pages
bypass_cdn_cache
end
end
# app/views/products/show.html.erb
<div class="product-page">
<!-- Cached by CDN -->
<h1><%= @product.name %></h1>
<div class="description"><%= @product.description %></div>
<!-- User-specific content fetched via AJAX -->
<div id="user-actions" data-product-id="<%= @product.id %>">
Loading...
</div>
</div>
<script>
// Fetch user-specific content after page load
fetch(`/api/products/${productId}/user-actions`, {
credentials: 'include'
})
.then(response => response.json())
.then(data => {
document.getElementById('user-actions').innerHTML = data.html;
});
</script>
Cache Invalidation Strategy implements a comprehensive approach to keeping CDN content fresh after deployments and content updates:
# app/services/cdn_invalidation_service.rb
class CdnInvalidationService
def initialize(provider: :cloudfront)
@provider = provider
@client = build_client
end
def invalidate_deployment
# Invalidate all assets and key pages after deployment
paths = [
'/assets/*',
'/packs/*',
'/index.html',
'/sitemap.xml'
]
invalidate_paths(paths)
end
def invalidate_product(product)
# Invalidate product-specific pages
paths = [
"/products/#{product.id}",
"/products/#{product.slug}",
"/api/products/#{product.id}.json"
]
invalidate_paths(paths)
purge_tags(["product-#{product.id}"])
end
def invalidate_category(category)
# Invalidate category and related pages
paths = [
"/categories/#{category.id}",
"/categories/#{category.slug}"
]
invalidate_paths(paths)
purge_tags(["category-#{category.id}"])
end
private
def build_client
case @provider
when :cloudfront
require 'aws-sdk-cloudfront'
Aws::CloudFront::Client.new
when :cloudflare
require 'cloudflare'
Cloudflare.connect(
key: ENV['CLOUDFLARE_API_KEY'],
email: ENV['CLOUDFLARE_EMAIL']
)
end
end
def invalidate_paths(paths)
case @provider
when :cloudfront
@client.create_invalidation({
distribution_id: ENV['CLOUDFRONT_DISTRIBUTION_ID'],
invalidation_batch: {
paths: { quantity: paths.size, items: paths },
caller_reference: Time.now.to_i.to_s
}
})
when :cloudflare
zone = @client.zones.find_by_name(ENV['DOMAIN'])
full_urls = paths.map { |p| "https://#{ENV['DOMAIN']}#{p}" }
zone.purge_cache(files: full_urls)
end
end
def purge_tags(tags)
return unless @provider == :cloudflare
zone = @client.zones.find_by_name(ENV['DOMAIN'])
zone.purge_cache(tags: tags)
end
end
# Use in callbacks
class Product < ApplicationRecord
after_save :invalidate_cdn_cache
private
def invalidate_cdn_cache
return unless Rails.env.production?
CdnInvalidationService.new.invalidate_product(self)
end
end
Image Optimization with CDN Transformations leverages CDN image processing to serve appropriately sized images without storing multiple versions:
# app/helpers/cdn_image_helper.rb
module CdnImageHelper
def cdn_image_url(path, transformations = {})
base_url = "https://#{ENV['CLOUDFLARE_DOMAIN']}"
if transformations.any?
params = build_transformation_params(transformations)
"#{base_url}/cdn-cgi/image/#{params}/#{path}"
else
"#{base_url}/#{path}"
end
end
private
def build_transformation_params(transformations)
params = []
params << "width=#{transformations[:width]}" if transformations[:width]
params << "height=#{transformations[:height]}" if transformations[:height]
params << "quality=#{transformations[:quality] || 85}"
params << "format=auto" # Serve WebP to supporting browsers
params.join(',')
end
end
# Usage in views
<%= image_tag cdn_image_url('products/laptop.jpg', width: 800, quality: 90) %>
# => <img src="https://cdn.example.com/cdn-cgi/image/width=800,quality=90,format=auto/products/laptop.jpg">
# Responsive images with srcset
<img src="<%= cdn_image_url('hero.jpg', width: 400) %>"
srcset="<%= cdn_image_url('hero.jpg', width: 400) %> 400w,
<%= cdn_image_url('hero.jpg', width: 800) %> 800w,
<%= cdn_image_url('hero.jpg', width: 1200) %> 1200w"
sizes="(max-width: 600px) 400px, (max-width: 1200px) 800px, 1200px">
Performance Considerations
CDN performance depends on configuration choices, content characteristics, and usage patterns. Optimizing these factors maximizes cache hit ratios and minimizes latency.
Cache Hit Ratio Optimization begins with appropriate TTL configuration. Static assets with versioned filenames—Rails asset pipeline output—should use maximum TTLs (1 year) since new content uses different URLs. HTML pages require shorter TTLs to ensure freshness but still benefit from CDN caching. API responses vary: slowly changing data like product catalogs can use moderate TTLs (5-15 minutes), while user-specific data requires bypass or very short TTLs.
# app/controllers/concerns/cache_control.rb
module CacheControl
CACHE_DURATIONS = {
static_asset: 1.year,
html_page: 5.minutes,
api_public: 15.minutes,
api_user: 0,
sitemap: 1.day,
feed: 1.hour
}
def set_cache_headers(type, revalidate: false)
duration = CACHE_DURATIONS[type]
if duration.zero?
response.headers['Cache-Control'] = 'private, no-cache'
else
directives = ['public', "max-age=#{duration.to_i}"]
directives << 'must-revalidate' if revalidate
response.headers['Cache-Control'] = directives.join(', ')
end
end
end
Compression significantly reduces bandwidth and improves load times. CDNs typically handle compression automatically, but origin servers must send appropriate headers. Brotli compression provides better compression than gzip for text content and is supported by modern CDNs.
# config/application.rb
module MyApp
class Application < Rails::Application
# Enable Rack::Deflater for compression
config.middleware.use Rack::Deflater
# Configure compression for specific content types
config.middleware.insert_before Rack::Deflater, Rack::Brotli
end
end
# Custom middleware for compression control
class CompressionControl
COMPRESSIBLE_TYPES = %w[
text/html
text/css
text/plain
text/javascript
application/javascript
application/json
application/xml
image/svg+xml
]
def initialize(app)
@app = app
end
def call(env)
status, headers, body = @app.call(env)
content_type = headers['Content-Type']&.split(';')&.first
if COMPRESSIBLE_TYPES.include?(content_type)
headers['Vary'] = [headers['Vary'], 'Accept-Encoding'].compact.join(', ')
end
[status, headers, body]
end
end
Connection Optimization through HTTP/2 and HTTP/3 improves performance by multiplexing requests over a single connection. Modern CDNs support these protocols automatically, but origin servers should also support them. HTTP/2 eliminates the need for domain sharding, as it removes the per-domain connection limit.
Prefetching and Preconnecting hints browsers to establish connections early, reducing latency for CDN requests:
<!-- app/views/layouts/application.html.erb -->
<head>
<!-- Preconnect to CDN domain -->
<link rel="preconnect" href="https://<%= ENV['CDN_DOMAIN'] %>">
<link rel="dns-prefetch" href="https://<%= ENV['CDN_DOMAIN'] %>">
<!-- Preload critical assets -->
<link rel="preload" as="style" href="<%= asset_path('application.css') %>">
<link rel="preload" as="script" href="<%= asset_path('application.js') %>">
<link rel="preload" as="font" type="font/woff2"
href="<%= asset_path('fonts/primary.woff2') %>" crossorigin>
</head>
Monitoring Cache Performance tracks CDN effectiveness through metrics including cache hit ratio, origin request volume, bandwidth savings, and P95 latency. Most CDN providers offer analytics dashboards, but applications can also track metrics:
# app/services/cdn_metrics_service.rb
class CdnMetricsService
def initialize
@cloudwatch = Aws::CloudWatch::Client.new(region: 'us-east-1')
@distribution_id = ENV['CLOUDFRONT_DISTRIBUTION_ID']
end
def get_cache_metrics(start_time:, end_time:)
metrics = {}
# Requests metric
metrics[:requests] = get_metric_statistics(
metric_name: 'Requests',
start_time: start_time,
end_time: end_time,
statistic: 'Sum'
)
# Bytes downloaded
metrics[:bytes_downloaded] = get_metric_statistics(
metric_name: 'BytesDownloaded',
start_time: start_time,
end_time: end_time,
statistic: 'Sum'
)
# Cache hit rate (calculated from CacheHitRate metric)
metrics[:cache_hit_rate] = get_metric_statistics(
metric_name: 'CacheHitRate',
start_time: start_time,
end_time: end_time,
statistic: 'Average'
)
metrics
end
private
def get_metric_statistics(metric_name:, start_time:, end_time:, statistic:)
@cloudwatch.get_metric_statistics({
namespace: 'AWS/CloudFront',
metric_name: metric_name,
dimensions: [{
name: 'DistributionId',
value: @distribution_id
}],
start_time: start_time,
end_time: end_time,
period: 3600,
statistics: [statistic]
}).datapoints
end
end
Security Implications
CDNs introduce security considerations spanning content protection, origin isolation, and access control. Understanding these aspects ensures secure CDN implementation.
Origin Protection prevents direct access to origin servers, forcing all traffic through the CDN. Without origin protection, attackers can bypass CDN security features and overwhelm the origin with requests. CloudFront supports origin access identity for S3 origins and custom headers for HTTP origins:
# app/middleware/cloudfront_verification.rb
class CloudfrontVerification
def initialize(app)
@app = app
@shared_secret = ENV['CLOUDFRONT_SHARED_SECRET']
end
def call(env)
request = Rack::Request.new(env)
# Verify request came from CloudFront
unless valid_cloudfront_request?(request)
return [403, {'Content-Type' => 'text/plain'}, ['Forbidden']]
end
@app.call(env)
end
private
def valid_cloudfront_request?(request)
# Check custom header set in CloudFront origin settings
request.get_header('HTTP_X_CUSTOM_SECRET') == @shared_secret
end
end
# config/application.rb
config.middleware.insert_before ActionDispatch::Static, CloudfrontVerification
HTTPS Enforcement ensures encrypted communication between users and edge servers. All modern CDNs support free SSL certificates through Let's Encrypt or provide custom certificate options. Rails applications should enforce HTTPS to prevent mixed content warnings:
# config/environments/production.rb
config.force_ssl = true
config.ssl_options = {
redirect: {
exclude: ->(request) {
# Allow health checks without SSL redirect
request.path == '/health'
}
},
hsts: {
expires: 1.year,
subdomains: true,
preload: true
}
}
Signed URLs restrict access to private content by generating time-limited URLs with cryptographic signatures. This approach works for protected downloads, premium content, and user-specific resources:
# app/services/signed_url_generator.rb
class SignedUrlGenerator
def initialize
@signer = Aws::CloudFront::UrlSigner.new(
key_pair_id: ENV['CLOUDFRONT_KEY_PAIR_ID'],
private_key: ENV['CLOUDFRONT_PRIVATE_KEY']
)
@domain = ENV['CLOUDFRONT_DOMAIN']
end
def generate(path, expires_in: 1.hour, ip_address: nil)
url = "https://#{@domain}/#{path}"
options = {
expires: Time.now + expires_in
}
# Optional: restrict to specific IP
if ip_address
options[:ip_address] = ip_address
end
@signer.signed_url(url, options)
end
def generate_policy_url(path, conditions: {})
policy = {
'Statement' => [{
'Resource' => "https://#{@domain}/#{path}",
'Condition' => {
'DateLessThan' => {'AWS:EpochTime' => (Time.now + 1.hour).to_i}
}.merge(conditions)
}]
}
@signer.signed_url("https://#{@domain}/#{path}", policy: policy.to_json)
end
end
# app/controllers/downloads_controller.rb
class DownloadsController < ApplicationController
before_action :authenticate_user!
def show
@file = File.find(params[:id])
# Verify user has access
unless @file.accessible_by?(current_user)
return render status: :forbidden
end
# Generate signed URL
generator = SignedUrlGenerator.new
signed_url = generator.generate(
@file.cdn_path,
expires_in: 15.minutes,
ip_address: request.remote_ip
)
redirect_to signed_url, allow_other_host: true
end
end
Content Security Policy (CSP) requires careful configuration when using CDNs. The CSP header must allow asset loading from CDN domains:
# config/initializers/content_security_policy.rb
Rails.application.config.content_security_policy do |policy|
cdn_domain = ENV['CDN_DOMAIN']
policy.default_src :self
policy.font_src :self, cdn_domain
policy.img_src :self, cdn_domain, :data
policy.object_src :none
policy.script_src :self, cdn_domain
policy.style_src :self, cdn_domain
# Allow inline styles for Asset Pipeline in development
if Rails.env.development?
policy.style_src :self, cdn_domain, :unsafe_inline
end
end
# Report violations
Rails.application.config.content_security_policy_report_only = false
Rails.application.config.content_security_policy_nonce_generator =
->(request) { SecureRandom.base64(16) }
DDoS Protection comes built into most CDN services through request rate limiting, IP reputation filtering, and traffic analysis. Applications can enhance protection by implementing additional rate limiting at the origin:
# app/middleware/rate_limiter.rb
class RateLimit
def initialize(app, options = {})
@app = app
@limit = options[:limit] || 100
@period = options[:period] || 1.minute
@redis = Redis.new(url: ENV['REDIS_URL'])
end
def call(env)
request = Rack::Request.new(env)
# Bypass rate limiting for CDN health checks
if request.get_header('HTTP_X_CLOUDFRONT_HEALTH_CHECK')
return @app.call(env)
end
key = "rate_limit:#{client_identifier(request)}"
count = @redis.incr(key)
if count == 1
@redis.expire(key, @period)
end
if count > @limit
return [429, {'Content-Type' => 'text/plain'}, ['Too Many Requests']]
end
@app.call(env)
end
private
def client_identifier(request)
# Prefer CloudFront viewer IP header
request.get_header('HTTP_CLOUDFRONT_VIEWER_ADDRESS') ||
request.ip
end
end
Tools & Ecosystem
The CDN ecosystem includes major providers, Ruby integration gems, and supporting tools for monitoring and optimization.
Major CDN Providers offer different capabilities and pricing models. CloudFront integrates deeply with AWS services, providing S3 origin support, Lambda@Edge for edge computing, and comprehensive analytics. Cloudflare emphasizes security features including WAF, DDoS protection, and bot management, while also offering generous free tiers. Fastly targets developers with real-time purging, VCL customization, and edge computing through Compute@Edge. Akamai serves enterprise customers with the largest CDN network and advanced optimization features.
Provider selection depends on requirements including geographic coverage, feature needs, pricing structure, and existing infrastructure. Applications already using AWS benefit from CloudFront integration, while applications requiring advanced security features might prefer Cloudflare.
# Gemfile
# AWS CloudFront
gem 'aws-sdk-cloudfront'
gem 'aws-sdk-s3'
# Cloudflare
gem 'cloudflare'
# Fastly
gem 'fastly'
# Multi-CDN support
gem 'cdn_uploader', github: 'your-org/cdn_uploader'
Asset Management Gems handle asset uploading and synchronization. The asset_sync gem specifically targets Rails asset pipeline integration with S3 and CloudFront. The fog gem provides abstracted cloud storage access across multiple providers including AWS, Google Cloud, and Azure.
# config/initializers/fog.rb
connection = Fog::Storage.new(
provider: 'AWS',
aws_access_key_id: ENV['AWS_ACCESS_KEY_ID'],
aws_secret_access_key: ENV['AWS_SECRET_ACCESS_KEY'],
region: 'us-east-1'
)
# Upload file to S3
directory = connection.directories.get(ENV['ASSETS_BUCKET'])
directory.files.create(
key: 'assets/logo.png',
body: File.open('app/assets/images/logo.png'),
public: true,
cache_control: 'public, max-age=31536000'
)
Monitoring Tools track CDN performance and costs. CDN providers offer native dashboards showing cache hit ratios, bandwidth usage, and request patterns. Third-party tools like Datadog and New Relic integrate with multiple CDN providers for unified monitoring:
# app/services/cdn_monitor.rb
class CdnMonitor
def initialize
@statsd = Datadog::Statsd.new('localhost', 8125)
end
def track_request(path, cache_status)
tags = [
"path:#{path}",
"cache_status:#{cache_status}"
]
@statsd.increment('cdn.request', tags: tags)
if cache_status == 'hit'
@statsd.increment('cdn.cache_hit', tags: tags)
else
@statsd.increment('cdn.cache_miss', tags: tags)
end
end
def track_bandwidth(bytes)
@statsd.histogram('cdn.bandwidth_bytes', bytes)
end
end
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
around_action :track_cdn_metrics
private
def track_cdn_metrics
start = Time.now
yield
duration = Time.now - start
cache_status = response.headers['X-Cache'] || 'unknown'
CdnMonitor.new.track_request(
request.path,
cache_status
)
end
end
Testing Tools verify CDN configuration and behavior. Command-line tools inspect response headers to confirm caching behavior, while specialized tools like webpagetest.org analyze complete page load performance including CDN impact:
Automated testing verifies CDN integration in CI/CD pipelines:
# spec/features/cdn_spec.rb
require 'rails_helper'
RSpec.describe 'CDN Integration', type: :feature do
it 'serves assets from CDN domain' do
visit root_path
css_link = page.find('link[rel=stylesheet]', visible: false)
expect(css_link[:href]).to start_with("https://#{ENV['CDN_DOMAIN']}")
end
it 'sets appropriate cache headers for static assets' do
response = Net::HTTP.get_response(
URI("https://#{ENV['CDN_DOMAIN']}/assets/application.css")
)
expect(response['Cache-Control']).to include('max-age=31536000')
expect(response['Cache-Control']).to include('public')
end
it 'includes CDN domain in CSP header' do
visit root_path
csp = page.response_headers['Content-Security-Policy']
expect(csp).to include(ENV['CDN_DOMAIN'])
end
end
Reference
HTTP Cache Headers
| Header | Purpose | Example Value |
|---|---|---|
| Cache-Control | Controls caching behavior | public, max-age=3600 |
| Expires | Legacy cache expiration | Wed, 21 Oct 2026 07:28:00 GMT |
| ETag | Content validation token | 686897696a7c876b7e |
| Last-Modified | Content modification time | Wed, 21 Oct 2024 07:28:00 GMT |
| Vary | Cache key variations | Accept-Encoding, Accept |
| Surrogate-Key | Cache tag for purging | product-123 category-5 |
| Surrogate-Control | CDN-specific cache control | max-age=3600 |
Cache-Control Directives
| Directive | Effect | Use Case |
|---|---|---|
| public | Allow caching by any cache | Static assets, public pages |
| private | Allow caching only by browser | User-specific content |
| no-cache | Must revalidate before use | Frequently updated content |
| no-store | Never cache | Sensitive data, CSRF tokens |
| max-age | Cache duration in seconds | All cached content |
| s-maxage | CDN-specific cache duration | Different TTL for CDN vs browser |
| must-revalidate | Strict expiration enforcement | Critical content |
| immutable | Content never changes | Fingerprinted assets |
Common TTL Strategies
| Content Type | TTL | Rationale |
|---|---|---|
| Static assets with fingerprints | 1 year | Never change, new versions use new URLs |
| Images with stable URLs | 1 week | Rarely change, acceptable staleness |
| HTML pages | 5 minutes | Balance freshness and cache benefit |
| API responses (public data) | 15 minutes | Acceptable staleness for most reads |
| API responses (user data) | 0 | Must be current |
| Sitemaps and feeds | 1 hour | Updated regularly but not critical |
| JavaScript/CSS bundles | 1 year | Fingerprinted via webpack/sprockets |
Rails Configuration Options
| Configuration | Purpose | Example |
|---|---|---|
| config.asset_host | CDN hostname for assets | https://cdn.example.com |
| config.action_controller.asset_host | Override for controller assets | Same as config.asset_host |
| config.assets.digest | Enable fingerprinting | true |
| config.assets.compile | Runtime compilation | false in production |
| config.public_file_server.headers | Headers for static files | Cache-Control header |
CloudFront Response Headers
| Header | Source | Meaning |
|---|---|---|
| X-Cache | CloudFront | Hit from cloudfront or Miss from cloudfront |
| X-Amz-Cf-Pop | CloudFront | Edge location serving request |
| X-Amz-Cf-Id | CloudFront | Request identifier |
| Age | CDN/Browser | Seconds object in cache |
| Via | Proxy | Proxy servers in chain |
Asset Sync Configuration
| Option | Purpose | Default |
|---|---|---|
| fog_provider | Cloud provider | AWS |
| fog_directory | S3 bucket name | Required |
| fog_region | AWS region | us-east-1 |
| existing_remote_files | Handle old files | keep |
| gzip_compression | Compress before upload | false |
| manifest | Use manifest for sync | false |
| cache_asset_regexps | Patterns to cache | Empty |
CDN Provider Comparison
| Feature | CloudFront | Cloudflare | Fastly | Akamai |
|---|---|---|---|---|
| Global PoPs | 400+ | 300+ | 70+ | 4000+ |
| Free Tier | No | Yes (generous) | No | No |
| Edge Computing | Lambda@Edge | Workers | Compute@Edge | EdgeWorkers |
| Purge API | Yes | Yes | Instant | Yes |
| Custom SSL | Yes (ACM) | Yes (Universal) | Yes | Yes |
| Origin Shield | Yes | Yes | No | Yes |
| Real-time Logs | Yes | Yes | Yes | Yes |
| Pricing Model | Pay per GB | Flat rate + usage | Pay per request | Enterprise |