---
title: "RR004 – Use ActiveJob for Background Work"
impact: high
impactDescription: "Spawning raw threads or doing heavy work inline blocks the Puma thread, causing request timeouts and memory leaks."
tags: [ruby, rails, performance, background-jobs]
---

# RR004 – Use ActiveJob for Background Work

## Rule

Any operation that is slow (email, external API, report generation, file processing) or must retry on failure must be enqueued via `ActiveJob`, not executed inline in the request or in a raw `Thread.new`.

## Why

- `Thread.new` in a Rails controller shares the request's database connection until it's reclaimed, causing connection pool exhaustion.
- Inline work blocks the Puma thread until completion — the user waits.
- ActiveJob provides retry semantics, dead-letter queues, and observable failure via your queue backend (Sidekiq, GoodJob, Solid Queue).

## Wrong

```ruby
# Inline — user waits for CSV export to complete
def export
  @orders = Order.all
  csv = generate_csv(@orders)   # could take 10+ seconds
  send_data csv, filename: 'orders.csv'
end

# Raw thread — races on connection pool, no retry, no observability
def send_welcome_email
  Thread.new { UserMailer.welcome(@user).deliver_now }
  redirect_to root_path
end
```

## Correct

```ruby
# app/jobs/generate_order_export_job.rb
class GenerateOrderExportJob < ApplicationJob
  queue_as :exports
  retry_on Net::TimeoutError, wait: 5.seconds, attempts: 3
  discard_on ActiveRecord::RecordNotFound

  def perform(user_id, filters)
    user = User.find(user_id)
    csv = OrderExportService.new(filters).generate
    UserMailer.export_ready(user, csv).deliver_now
  end
end

# Controller
def export
  GenerateOrderExportJob.perform_later(current_user.id, export_params.to_h)
  redirect_to orders_path, notice: 'Export will be emailed to you.'
end

# Mailer — always defer with deliver_later
UserMailer.welcome(@user).deliver_later
```

## Notes

- Use `perform_later` (async) in production; `perform_now` only in tests / sync scenarios.
- Configure the queue backend in `config/application.rb`: `config.active_job.queue_adapter = :sidekiq`.
- Serialize only primitive-safe arguments (IDs, not AR objects) — AR objects go stale between enqueue and perform.
