Active Storage is a wonderful library for managing file uploads in Rails. But terms like blob and attachment are easy to mix up, and that confusion can remain even after you’ve been using Active Storage regularly. I’ve run into that myself. I’ve used Active Storage for a long time in several projects, without ever really understanding how models, blobs and attachments worked together.
So I decided to read up and really understand the whole system, and what follows is my understanding of the Active Storage domain model. Once you understand their roles, it becomes easier to reason about file uploads, deletions, and reuse. This understanding is really useful as your app grows over time and you have to move, delete, or replace documents and the models they're attached to.
In this post, we'll only focus on the blobs and attachments for now, and explore variants in a later post, along with the other parts of Active Storage.
Here's the quick gist of it:
- A blob is the data about the uploaded file and a pointer to the actual file.
- An attachment is “this record uses that file under this name.”

Blob
Represents stores metadata about the uploaded file. The actual file is stored in the storage service, such as AWS S3, disk, GCS, etc. The blob holds the metadata and the storage key for where that file resides on the service.
Database Table: active_storage_blobs
Columns:
key
: the path/key used to fetch the bytes from the storage servicefilename
: original filenamecontent_type
: MIME type of the filebyte_size
: Size of the file in number of byteschecksum
: To check the integrity of the filemetadata
: JSON (image dimensions, analyzed flags, etc.)service_name
: which storage service this blob lives on- Timestamps
A blob is meant to represent a specific, unchanging file. Once a blob is created, it should always point to the same file data. That’s part of the design. Now you can update some of its descriptive information later (like metadata stored in the database), but you should never change the actual file it refers to or alter its storage key.
If you need to modify the file, for example, to create a resized version of an image, add a watermark, or replace it with a new upload, the right approach is to create a brand-new blob for the new file and, if the old one is no longer needed, purge (delete) the original blob.
This “immutability” makes blobs predictable and safe to reference throughout your app. You always know that a given blob refers to the exact same bytes it did when it was created.
Attachment
A polymorphic join table that connects a blob to your application model. An attachment belongs to a record and to a blob. A blob doesn't belong to a record. To get the blob from a record, you have to use the attachment.
Attachment
is the join row: it links one model record + a named slot (e.g. "avatar"
, "photos"
) to one Blob
.
Check out the following post to learn more about polymorphic associations in Rails.

Database table: active_storage_attachments
Columns:
name
: the association name on your model (:avatar
,:files
, …)record_type
/record_id
: which model/row is attachedblob_id
: which blob it’s pointing at- Timestamp
Here's how they all work together.

record_type
(class name string), if you rename or refactor the model (say from User
→ AccountUser
), existing active_storage_attachments
rows will still reference the old class name string. You’ll need a migration to update those rows.Consider the following models in a Rails app.
class User
has_one_attached :avatar
end
class Project
has_many_attached :documents
end
has_one_attached :avatar
or has_many_attached :documents
defines the attachment association.
When you do user.avatar.attach(...)
, Active Storage does three things:
- Create a blob to represent that uploaded file and store its metadata
- Upload the file to the storage service
- Create an attachment that links the
user
model record to that blob.
Note: If the record is persisted and unchanged, the attachment is saved to the database immediately. Otherwise, it'll be saved to the DB when the record is next saved.
A blob can be attached multiple times (to same or different models - though I think it's not that common). This is how you reuse the underlying file without having to upload it again. Each usage creates a separate attachment row pointing to the same blob and the different records.
Note: For direct uploads, Rails will create a blob by uploading the file directly to S3 from the client. The attachment is created later when the form is submitted. If the form is abandoned, you'll end up with unattached blobs, which can be cleared with the active_storage:purge_unattached
task. We'll cover direct uploads in a future post, but wanted to mention this here.
In your app, you'll typically perform the following operations:
user.avatar # => returns an ActiveStorage::Attached::One proxy
user.avatar.attached? # => true/false
user.avatar.attachment # => the ActiveStorage::Attachment record (join)
user.avatar.blob # => the ActiveStorage::Blob record (file metadata)
user.avatar.download # => downloads the blob bytes into memory
user.avatar.open do |file| # => opens a Tempfile with the blob content
# do something with file.path, etc.
end
Replacing and Deleting Things
If you want to delete the attachment, i.e. the link between a record and a blob, without deleting the actual file, use the detach
method on the model, i.e. user.avatar.detach
. This will destroy the Attachment
row while keeping the file as-is. If another record still uses that blob, it stays intact.

If you want to delete both the attachment and the blob, use the purge
method, e.g. user.avatar.purge
. This will remove the actual file from the storage service. Remember: This will make a network call to the storage service. To do this in a background job, use the purge_later
method instead.

What can you do with a Blob?
Once you understand that a blob represents a specific uploaded file, the next question is: what can you actually do with it?
It turns out ActiveStorage::Blob
gives you a rich API to work with files in your Rails app. From reading metadata to downloading bytes, generating URLs, analyzing content, and even creating new blobs manually, all without ever touching the underlying storage service directly.
Here’s a practical tour of the most useful things you can do with a blob.
1. Generate signed URLs for direct access
One of the most common use cases is to give the browser a link to download or view the file. Rails can generate secure, expiring URLs:
rails_blob_url(blob)
# => "https://yourapp.com/rails/active_storage/blobs/.../profile.png"
Or, for inline display in an <img>
tag:
<%= image_tag url_for(user.avatar) %>
These URLs are signed, meaning they expire and can’t be guessed.
2. Access metadata about the file
Every blob stores details about the uploaded file. You can access these directly:
blob = post.cover_image.blob
blob.filename # => #<ActiveStorage::Filename "autumn.jpg">
blob.content_type # => "image/jpeg"
blob.byte_size # => 245183
blob.checksum # => "Yf1K1Gk...==" # MD5 checksum of the content
blob.metadata # => { "identified" => true, "width" => 1200, "height" => 630 }
blob.service_name # => "amazon"
This is useful when you need to display file info in the UI, validate content types, or log uploads.
3. Read or download the actual file data
A blob is linked to a file in storage service, and you can access those bytes in several ways:
blob.download # => returns the entire file as a string (useful for small files)
blob.open do |file| # => gives you a Tempfile to work with
puts file.path
# do something with the file, e.g., send it to another service
end
You can also stream the data in chunks if the file is large. Each chunk is yielded to the block.
blob.download do |chunk|
process(chunk) # handle data incrementally
end
4. Trigger analysis to extract metadata
For certain file types (like images, videos, or PDFs), Active Storage can automatically extract metadata such as dimensions, duration, or page count.
blob.analyze
blob.analyzed? # => true
blob.metadata # => { "width" => 800, "height" => 600, "analyzed" => true }
5. Create blobs manually (for scripts or background jobs)
You don’t always need to go through an attach
call. It's fine to create blobs directly. This is useful in background tasks or service objects.
blob = ActiveStorage::Blob.create_and_upload!(
io: File.open("/path/to/report.pdf"),
filename: "report.pdf",
content_type: "application/pdf"
)
# attach it later
user.documents.attach(blob)
Or if you already have the file in memory:
blob = ActiveStorage::Blob.create_after_upload!(
io: StringIO.new(pdf_content),
filename: "generated.pdf",
content_type: "application/pdf"
)
I hope this post was helpful in somewhat clarifying the Active Storage domain model. We'll continue with the other aspects of Active Storage in future posts.
I hope you found this article helpful and you learned something new.
As always, if you have any questions or feedback, didn't understand something, or found a mistake, please leave a comment below or send me an email. I reply to all emails I get from developers, and I look forward to hearing from you.
If you'd like to receive future articles directly in your email, please subscribe to my blog. Your email is respected, never shared, rented, sold or spammed. If you're already a subscriber, thank you.