2 minutes
Efficient Background Processing with Active Job and Async Querying
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:
- Chunk data: Split a large dataset into pages.
- Parallelize: Use Ractors or Sidekiq’s concurrency to generate pages in parallel.
- Stream to storage: Write each page via
async_query
or direct file streams. - 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.