Introduction

Background jobs are the workhorses of a scaled Rails app: they handle sending emails, processing uploads, generating reports, and more. Rails’ Active Job provides a unified interface for different backends, and with the new async_query helper and fine‑tuned queue adapters, you can squeeze maximum performance out of your job system.

In this post, we’ll explore:

  • The async_query helper for non‑blocking database access
  • Choosing and tuning queue adapters (Sidekiq, Async, etc.)
  • Chaining jobs with Action Mailbox/Text pipelines
  • Case study: scaling PDF report generation

Async Query Helper

Long-running database queries in jobs can tie up threads or processes. Rails’ async_query blocks free up the main thread while the query runs:

class ReportJob < ApplicationJob
  def perform(user_id)
    records = async_query do
      Record.where(user_id: user_id).to_a
    end

    generate_pdf(records)
  end
end

Behind the scenes, async_query runs the block in a separate thread or process, letting the job worker handle other tasks meanwhile.

Queue Adapter Tuning

Active Job supports multiple adapters. Picking and configuring the right one is crucial:

  • Sidekiq: High throughput; configure concurrency in sidekiq.yml:

    :concurrency: 25
    :queues:
      - default
      - mailers
    
  • Async: Built‑in, zero dependencies, great for low‑volume apps.

  • Delayed Job: Simple but slower; good for legacy upgrades.

Tips:

  • Use dedicated queues for critical or heavy jobs.
  • Set sensible timeouts and retry backoffs.
  • Monitor queue depths via Sidekiq Web UI or Grafana dashboards.

Action Mailbox/Text Pipelines

Chaining jobs manually can be verbose. Rails’ Action Mailbox/Text pipelines simplify this:

# app/mailboxes/application_mailbox.rb
class ApplicationMailbox < ActionMailbox::Base
  routing /report/i => :report
end

class ReportMailbox < ApplicationMailbox
  def process
    ProcessReportJob.perform_later(mail.record_id)
  end
end

For SMS pipelines using Action Text or third‑party services, the pattern is similar: incoming messages trigger jobs in sequence.

Case Study: Scaling PDF Generation

A PDF report that once took 30 seconds can be optimized:

  1. Chunk data: Split a large dataset into pages.
  2. Parallelize: Use Ractors or Sidekiq’s concurrency to generate pages in parallel.
  3. Stream to storage: Write each page via async_query or direct file streams.
  4. Retry failed slices: Use a dead‑letter queue for any pages that error.
class PdfReportJob < ApplicationJob
  def perform(report_id)
    Report.find(report_id).pages.in_groups_of(10).each do |group|
      GeneratePageJob.perform_later(group.map(&:id))
    end
  end
end

Conclusion

By combining Active Job’s unified API with async_query, targeted queue configurations, and pipeline patterns, Rails apps can tackle heavy background workloads efficiently. Whether you’re sending thousands of emails or generating complex reports, these techniques will help keep your job system fast and reliable.