Search... ⌘K
Write Log In Sign Up

Join DevSolved

Share war stories & investigate outages.

Sign Up for Free Log In to Account
ESC
NORMAL Resolved

Ruby on Rails: The ActiveStorage Bloat OOM

Avatar Devsolved Feb 20, 2024 — 11:00 UTC MTTR: 8 hours 2 min read 92000 views
Executive Summary

Sidekiq workers processing Excel exports started consuming gigabytes of RAM until the Linux OOM Killer destroyed the EC2 instances. The culprit was a hidden buffer in Rails ActiveStorage.

The Symptom

Symptom

Users clicking the "Export All Transactions" button were complaining that they never received the email with their CSV file. Looking at the AWS console, our background worker EC2 instances were constantly crashing and rebooting. Syslogs revealed `Out of memory: Killed process (ruby)`.

Root Cause Analysis

Root Cause

We used the `axlsx` gem to generate Excel files, and then attached the generated file to an `Export` ActiveRecord model using ActiveStorage (`export.file.attach(io: File.open(temp_file))`). However, because the files were generated locally on the disk, ActiveStorage decided it needed to read the *entire* file into a string in RAM to calculate the MD5 checksum before uploading it to S3. For a 2GB Excel file, this forced Ruby to allocate 2GB of contiguous memory, instantly crashing the 2GB worker node.

Code ruby
# The fatal flaw: ActiveStorage reading whole files into memory for checksums
class ExportWorker
  def perform(export_id)
    export = Export.find(export_id)
    temp_file = generate_massive_excel(export)
    
    # This line reads the entire 2GB file into RAM instantly
    export.file.attach(io: File.open(temp_file), filename: "export.xlsx")
  end
end

The abstraction provided by ActiveStorage completely hid the fact that it was not streaming the upload, but rather buffering it into memory.

Resolution

Resolution

We bypassed ActiveStorage's automatic checksum generation for massive files by using the lower-level AWS SDK directly. We implemented a multipart streaming upload directly to S3. Once the file was safely in S3, we created the ActiveStorage `Blob` record manually and attached it to the model without pulling the file back into memory.
Code ruby
# The fix: Direct multipart S3 streaming
s3_resource = Aws::S3::Resource.new
obj = s3_resource.bucket("exports").object("temp/export.xlsx")
obj.upload_file(temp_file_path) # AWS SDK streams chunks efficiently

blob = ActiveStorage::Blob.create_before_direct_upload!(
  filename: "export.xlsx",
  byte_size: File.size(temp_file_path),
  checksum: "bypass" # Skip memory-heavy checksum
)
export.update(file: blob.signed_id)

Preventive Action Items

high Audit all `attach` calls for files potentially larger than 50MB @Backend Team
normal Implement presigned URLs for client-side direct uploads to bypass Ruby entirely where possible @Frontend Team
|

Discussions 0

Most recent