File uploads with ActiveStorage
Rails 5.2 finally puts file uploads in the framework with ActiveStorage. A tour of how it works — blobs, attachments, service backends, variants, and direct uploads — and an honest comparison with CarrierWave and Paperclip.
For years, file uploads in Rails meant reaching for a gem — CarrierWave or Paperclip — and wiring up your own storage, processing, and conventions. They worked, but every project reinvented the same setup, and the two leading gems had real friction (Paperclip was heading toward retirement, and both carried accumulated complexity). Rails 5.2 changes this by bringing file uploads into the framework with ActiveStorage. Having used all three, here is how ActiveStorage works and how it compares.
The model: blobs and attachments
ActiveStorage’s design is cleaner than the column-based approach the older gems used. Instead of
adding avatar_file_name, avatar_content_type, etc. columns to your model, it uses two of its
own tables: a blob (metadata about an uploaded file — filename, content type, byte size,
checksum, and where it lives in storage) and an attachment (a join linking a blob to one of
your models). You declare attachments declaratively:
class User < ApplicationRecord
has_one_attached :avatar
end
class Product < ApplicationRecord
has_many_attached :images
end
user.avatar.attach(params[:avatar]) # attach an uploaded file
user.avatar.attached? # => true
url_for(user.avatar) # a URL to serve it
has_one_attached and has_many_attached read just like has_one/has_many, which is the
point — uploads are now a first-class association concept, not a pile of columns and a gem’s
conventions. Your model table stays clean; the file metadata lives in ActiveStorage’s tables.
Service backends: local, S3, and friends
Where files are stored is configured separately from your code, through a service. Rails ships adapters for local disk, Amazon S3, Google Cloud Storage, and Azure, selected per environment:
# config/storage.yml
local:
service: Disk
root: <%= Rails.root.join("storage") %>
amazon:
service: S3
bucket: my-app-uploads
region: eu-west-1
# config/environments/production.rb
config.active_storage.service = :amazon
Use local disk in development and S3 in production by changing one line — your application code
(user.avatar.attach(...)) does not change at all. This separation is exactly right: where
files live is an environment concern, not an application one, and it embodies the same “store
files in object storage, not on the app server” lesson from the image-pipeline work, now built
into the framework.
Variants: image transformations on demand
For images, ActiveStorage generates variants — transformed versions (resized, cropped) —
through ImageMagick (via mini_magick/image_processing), and it does so lazily: the variant
is generated on first request and then cached:
<%= image_tag user.avatar.variant(resize_to_limit: [200, 200]) %>
This is the on-the-fly model from the image-pipeline discussion, built in: you ask for a size, ActiveStorage produces and caches it. It is convenient and removes a lot of the manual version-declaration the older gems required. (For a large catalog with a fixed set of sizes and heavy traffic, you may still want to think about eager generation and CDN caching, as discussed before — ActiveStorage gives you the mechanism, but the at-scale strategy is still yours.)
Direct uploads: skip the app server
A genuinely nice feature is direct uploads: ActiveStorage ships JavaScript that uploads the file directly from the browser to your storage service (S3), bypassing your app server entirely. Your Rails process is not tied up streaming a large file through itself; the browser sends it straight to S3 and then submits the form with a reference. For apps that handle big files or many concurrent uploads, this keeps your web workers free for actual requests — a real architectural win that the older gems did not offer out of the box.
ActiveStorage vs CarrierWave/Paperclip
The honest comparison, because “it’s in the framework now” does not automatically mean “switch everything”:
- ActiveStorage’s advantages: it is built in and maintained by the Rails team (no third-party gem to track), the blob/attachment model is clean, direct uploads are first-class, and the service abstraction is excellent. For a new app, it is the natural default.
- Where the older gems still lead (in 2018): CarrierWave is mature and offers fine-grained control over processing, conditional versions, and validation that ActiveStorage, being new, has not all caught up on yet. File-type and size validation, in particular, is notably less built-out in early ActiveStorage. So an existing CarrierWave app with complex needs has no urgent reason to migrate, and some edge cases are still easier in the older gems.
Our take: reach for ActiveStorage on new projects — it is the framework-blessed path and the right default going forward — while recognising it is young and will mature. Do not rush to rip out a working CarrierWave setup; migrate when you have a reason, not for fashion.
Verdict
ActiveStorage is a welcome and overdue addition: file uploads finally belong to Rails rather than to a gem you bolt on and configure from scratch. The blob/attachment model keeps your tables clean, the service abstraction makes local-vs-S3 a one-line config change, variants handle image transformation on demand, and direct uploads keep large files off your app servers. It is young in 2018 — validation and some advanced processing lag CarrierWave — so it is the clear default for new apps while not a mandate to migrate existing ones. As it matures it will become simply how you do uploads in Rails, the same way the framework has steadily absorbed every other piece of plumbing every app used to assemble by hand.