Rails Active Storage in 2026: File Uploads, S3 Integration and Interview Questions

Master Rails Active Storage for file uploads with S3 and direct uploads. Complete tutorial with code examples and common interview questions about file handling in Ruby on Rails.

Rails Active Storage file upload architecture with cloud storage integration

Rails Active Storage provides a unified API for attaching files to Active Record models, supporting local disk storage and cloud services like Amazon S3, Google Cloud Storage, and Azure Storage. Released with Rails 5.2 and refined through Rails 8.1, Active Storage handles the complexity of file uploads, transformations, and cloud integration behind a simple interface.

Direct Uploads Save Server Resources

With direct uploads enabled, browsers upload files straight to S3 using presigned URLs. The Rails server handles two small JSON requests instead of proxying large files, reducing memory usage and avoiding request timeouts on platforms like Heroku.

Setting Up Active Storage with S3 in Rails 8

Active Storage ships with Rails. The installation creates three tables: active_storage_blobs for file metadata, active_storage_attachments for polymorphic associations, and active_storage_variant_records for tracking generated variants.

bash
# Terminal commands
bin/rails active_storage:install
bin/rails db:migrate

For S3 integration, add the AWS SDK gem to the Gemfile. The require: false option delays loading until Active Storage needs it.

ruby
# Gemfile
gem "aws-sdk-s3", require: false

Configure the storage service in config/storage.yml. Store credentials in Rails credentials or environment variables, never in version control.

yaml
# config/storage.yml
amazon:
  service: S3
  access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %>
  secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %>
  region: us-east-1
  bucket: myapp-production-uploads

Set the storage service per environment. Development typically uses local disk, production uses S3.

ruby
# config/environments/production.rb
config.active_storage.service = :amazon

Attaching Files to Models with has_one_attached and has_many_attached

Active Storage uses two macros for attachments: has_one_attached for single files like avatars, and has_many_attached for collections like photo galleries.

ruby
# app/models/user.rb
class User < ApplicationRecord
  has_one_attached :avatar
  has_many_attached :documents
end

Attaching files happens through assignment. Active Storage accepts ActionDispatch::Http::UploadedFile from form submissions, File objects, or blobs.

ruby
# app/controllers/users_controller.rb
class UsersController < ApplicationController
  def update
    @user = User.find(params[:id])
    @user.avatar.attach(params[:avatar]) if params[:avatar].present?
    @user.update(user_params)
    redirect_to @user
  end

  private

  def user_params
    params.require(:user).permit(:name, :email, documents: [])
  end
end

Querying attachment presence uses attached?. For eager loading attachments to avoid N+1 queries, use the with_attached_* scope that Active Storage generates automatically.

ruby
# Checking attachment presence
if @user.avatar.attached?
  # Process avatar
end

# Eager loading to prevent N+1 queries
User.with_attached_avatar.where(active: true)

Direct Uploads: Browser-to-S3 File Transfer

Direct uploads bypass the Rails server entirely. The browser requests a presigned URL from Rails, then uploads the file directly to S3. This approach handles large files without server timeouts and reduces memory consumption.

Enable direct uploads by adding the direct_upload: true option to file fields.

erb
<%# app/views/users/_form.html.erb %>
<%= form_with model: @user do |form| %>
  <%= form.file_field :avatar, direct_upload: true %>
  <%= form.file_field :documents, multiple: true, direct_upload: true %>
  <%= form.submit %>
<% end %>

The @rails/activestorage JavaScript library handles the upload flow. Rails 8 applications using import maps get this automatically.

app/javascript/application.jsjavascript
import * as ActiveStorage from "@rails/activestorage"
ActiveStorage.start()

S3 buckets require CORS configuration to accept direct uploads from browsers. This policy allows uploads from any origin during development; restrict it in production.

json
[
  {
    "AllowedHeaders": ["*"],
    "AllowedMethods": ["PUT"],
    "AllowedOrigins": ["https://myapp.com"],
    "ExposeHeaders": ["Origin", "Content-Type", "Content-MD5", "Content-Disposition"],
    "MaxAgeSeconds": 3600
  }
]

Ready to ace your Ruby on Rails interviews?

Practice with our interactive simulators, flashcards, and technical tests.

Image Variants with libvips Processing

Active Storage generates image variants on-demand using the image_processing gem. Rails 8 defaults to libvips, which processes images faster than ImageMagick while using less memory.

ruby
# Gemfile
gem "image_processing", "~> 1.13"

Define variants inline or as named methods. Variants are generated lazily when first requested and cached for subsequent requests.

ruby
# Inline variant in view
<%= image_tag @user.avatar.variant(resize_to_limit: [200, 200]) %>

# Named variant for reuse
# app/models/user.rb
class User < ApplicationRecord
  has_one_attached :avatar do |attachable|
    attachable.variant :thumbnail, resize_to_limit: [100, 100]
    attachable.variant :medium, resize_to_limit: [300, 300]
  end
end

# Using named variant
<%= image_tag @user.avatar.variant(:thumbnail) %>

The July 2026 security update (CVE-2026-66066) blocks untrusted libvips loaders by default. Applications processing BMP, ICO, or PSD files need explicit configuration, and libvips must be version 8.13 or higher.

Validating File Uploads in Rails 8

Active Storage does not include built-in validations. Use the active_storage_validations gem or implement custom validators.

ruby
# Gemfile
gem "active_storage_validations"

# app/models/user.rb
class User < ApplicationRecord
  has_one_attached :avatar

  validates :avatar,
    content_type: [:png, :jpg, :jpeg, :webp],
    size: { less_than: 5.megabytes }
end

For custom validation without external gems, implement an Active Model validator.

ruby
# app/models/user.rb
class User < ApplicationRecord
  has_one_attached :avatar

  validate :acceptable_avatar

  private

  def acceptable_avatar
    return unless avatar.attached?

    allowed_types = ["image/png", "image/jpeg", "image/webp"]
    unless allowed_types.include?(avatar.content_type)
      errors.add(:avatar, "must be PNG, JPEG, or WebP")
    end

    if avatar.byte_size > 5.megabytes
      errors.add(:avatar, "must be less than 5MB")
    end
  end
end

S3 Alternatives: Cloudflare R2 and Backblaze B2

S3-compatible services like Cloudflare R2 offer significant cost savings. R2 provides 10GB free storage with zero egress fees. Configure these services using the S3 adapter with a custom endpoint.

yaml
# 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: myapp-uploads
  endpoint: https://<ACCOUNT_ID>.r2.cloudflarestorage.com

The Rails documentation on Active Storage covers additional configuration options for different storage providers.

Common Interview Questions About Active Storage

Technical interviews for Rails positions frequently include questions about file handling. These questions assess understanding of storage patterns, performance considerations, and security practices.

How does Active Storage differ from CarrierWave or Shrine?

Active Storage ships with Rails and requires no external dependencies beyond the cloud SDK gems. CarrierWave and Shrine are standalone gems with different philosophies. CarrierWave uses uploader classes for processing logic. Shrine emphasizes a plugin architecture with explicit configuration. Active Storage favors convention and integrates tightly with Action View helpers and Active Record associations. For more foundational concepts, the Active Storage module questions on SharpSkill cover the full range of interview topics.

What happens to attachments when a record is destroyed?

By default, Active Storage purges attachments asynchronously when the parent record is destroyed. The dependent option controls this behavior.

ruby
# Purge attachments synchronously on destroy
has_one_attached :avatar, dependent: :purge_later  # default
has_one_attached :avatar, dependent: :purge        # synchronous
has_one_attached :avatar, dependent: false         # keep files

How do direct uploads handle network failures?

The JavaScript library emits events during the upload lifecycle. Applications can listen for direct-upload:error to handle failures and provide user feedback.

app/javascript/application.jsjavascript
addEventListener("direct-upload:error", (event) => {
  const { id, error } = event.detail
  const element = document.getElementById(`direct-upload-${id}`)
  element.classList.add("upload-error")
  element.setAttribute("title", error)
})

How would you prevent N+1 queries when displaying multiple users with avatars?

Active Storage generates a with_attached_* scope for each attachment. This scope eager-loads the blob and attachment records.

ruby
# N+1 problem: separate query per avatar
User.all.each { |u| u.avatar.filename }  # Bad

# Solution: eager load attachments
User.with_attached_avatar.each { |u| u.avatar.filename }  # Good

For more Ruby on Rails interview questions, including ActiveRecord optimization and testing strategies, SharpSkill provides focused preparation materials.

Mirroring for Redundancy and Migration

Active Storage supports mirroring uploads to multiple services simultaneously. This pattern works for redundancy or when migrating between storage providers.

yaml
# config/storage.yml
production:
  service: Mirror
  primary: amazon
  mirrors:
    - cloudflare_r2

amazon:
  service: S3
  # ... S3 config

cloudflare_r2:
  service: S3
  # ... R2 config

With mirroring, uploads go to all services, but downloads come from the primary. After migration completes, switch the primary and remove the mirror.

Performance Optimization for Production Deployments

Several techniques improve Active Storage performance in production environments.

Proxy mode for private files: Active Storage can proxy downloads through Rails or redirect to signed URLs. Redirects are faster but require public files or signed URL support.

ruby
# config/environments/production.rb
config.active_storage.resolve_model_to_route = :rails_storage_proxy  # proxy
config.active_storage.resolve_model_to_route = :rails_storage_redirect  # redirect

Variant tracking: Rails 8.1 tracks generated variants in active_storage_variant_records. This table prevents regenerating the same variant repeatedly.

Background purging: Configure Active Job to process ActiveStorage::PurgeJob in a dedicated queue to avoid blocking other jobs.

ruby
# config/application.rb
config.active_storage.queues.purge = :low_priority

Start practicing!

Test your knowledge with our interview simulators and technical tests.

Key Takeaways for Rails Active Storage Implementation

  • Install Active Storage with bin/rails active_storage:install and configure S3 credentials in config/storage.yml using Rails credentials
  • Enable direct uploads with direct_upload: true on file fields to bypass server file handling and reduce memory pressure
  • Use with_attached_* scopes when loading multiple records with attachments to prevent N+1 queries
  • Add the active_storage_validations gem or implement custom validators since Active Storage has no built-in file validation
  • Consider S3-compatible alternatives like Cloudflare R2 for reduced egress costs
  • Update to Rails 8.1.3.1 or later for the CVE-2026-66066 security patch affecting libvips image processing
  • Use mirroring when migrating between storage providers to maintain file availability during transition
Daily challenge

Can you spot the bug in Ruby on Rails?

One real snippet, one hidden bug, one attempt a day. No account needed to try.

Anthony Fillion-Maillet

Written by

Anthony Fillion-Maillet

Founder of SharpSkill

Full-stack developer for over 10 years. Runs SharpSkill and answers for everything published here.

Updated on September 9, 2026

Tags

#ruby-on-rails
#active-storage
#file-upload
#s3
#aws

Share

Related articles