Does Ruby on Rails Work With Cloudflare R2?
Ruby on Rails works seamlessly with Cloudflare R2 using the aws-sdk-s3 gem, treating R2 as an S3-compatible storage backend with zero egress fees.
Quick Facts
How Ruby on Rails Works With Cloudflare R2
Rails integrates with Cloudflare R2 through ActiveStorage, which abstracts storage backends behind a consistent API. Since R2 is S3-compatible, you configure it using the aws-sdk-s3 gem by pointing credentials and endpoints to your R2 account. The developer experience is identical to using AWS S3—you attach files to models, generate signed URLs, and handle uploads the same way. Under the hood, Rails makes HTTP requests to R2's S3 API endpoint instead of Amazon's, but the gem handles all the authentication and protocol details. The major architectural benefit is cost: R2 charges zero egress fees, making it ideal for applications serving files to users globally or requiring frequent downloads. Setup takes minutes and requires only environment variables for credentials and bucket configuration.
Best Use Cases
Rails ActiveStorage with R2
bundle add aws-sdk-s3# config/storage.yml
cloudflare_r2:
service: S3
access_key_id: <%= ENV['R2_ACCESS_KEY_ID'] %>
secret_access_key: <%= ENV['R2_SECRET_ACCESS_KEY'] %>
region: auto
bucket: <%= ENV['R2_BUCKET'] %>
endpoint: <%= ENV['R2_ENDPOINT'] %>
force_path_style: true
# config/environments/production.rb
config.active_storage.service = :cloudflare_r2
# In your Rails model
class User < ApplicationRecord
has_one_attached :avatar
end
# Usage
user = User.find(1)
user.avatar.attach(io: File.open('photo.jpg'), filename: 'photo.jpg')
user.avatar.blob.service_url(expires_in: 1.hour)Known Issues & Gotchas
R2 API has minor compatibility gaps with S3 (e.g., some ACL operations, conditional request headers behave differently)
Fix: Stick to standard S3 operations in aws-sdk-s3. Avoid advanced ACL configurations and test edge cases in staging. Most Rails use cases won't hit these limits.
Signed URL expiration defaults differ; R2 URLs expire after 1 hour by default while S3 allows longer durations
Fix: Explicitly set expires_in parameter when generating signed URLs: `object.presigned_url(:get_object, expires_in: 3600)`
Regional endpoint configuration is required; R2 doesn't auto-detect like S3 does
Fix: Always set `endpoint` and `region` in storage.yml or credentials. Use format: `https://<account-id>.r2.cloudflarestorage.com`
Alternatives
- •AWS S3 + CloudFront (more mature but higher egress costs without CloudFront optimization)
- •Backblaze B2 with Rails (cheaper storage, requires custom integration, less native support)
- •Google Cloud Storage with Rails (strong integration, pay-per-use model, egress fees apply)
Resources
Related Compatibility Guides
Explore more compatibility guides