Active Job Coder
Job Design Principles
1. Single Responsibility
# Good: Focused job that delegates domain work
class SendWelcomeEmailJob < ApplicationJob
queue_as :default
def perform(user)
user.send_welcome_email
end
end
Keep job bodies shallow. The job should restore context, choose queue/retry behavior, and call a model or domain method that owns the actual work.
2. Pass Stable Payloads
# Good: pass identifiers for external boundaries or very stale-sensitive work
class ProcessOrderJob < ApplicationJob
def perform(order_id)
order = Order.find(order_id)
order.process
end
end
Passing Active Record instances is also valid in Rails when GlobalID semantics are the right contract. Avoid large object graphs, hashes of denormalized state, or relying on Current inside perform.
3. Queue Configuration
class CriticalNotificationJob < ApplicationJob
queue_as :critical
queue_with_priority 1 # Lower = higher priority
end
class ReportGenerationJob < ApplicationJob
queue_as :low_priority
queue_with_priority 50
end
The queue also selects the execution model when the backend runs mixed workers, since Solid Queue supports fibers: alongside threads:. Route by workload shape: I/O-bound jobs such as LLM calls, HTTP requests, and Turbo broadcasts belong on a fiber queue; CPU-bound work and jobs holding a transaction for their whole run belong on a thread queue. See solid-queue-coder for the worker config and pool sizing.
Error Handling & Retry Strategies
class ExternalApiJob < ApplicationJob
queue_as :default
retry_on Net::OpenTimeout, wait: :polynomially_longer, attempts: 5
retry_on ActiveRecord::Deadlocked, wait: 5.seconds, attempts: 3
discard_on ActiveJob::DeserializationError
def perform(record_id)
record = Record.find(record_id)
ExternalApi.sync(record)
end
end
Custom Error Handling
class ImportantJob < ApplicationJob
rescue_from StandardError do |exception|
Rails.logger.error("Job failed: #{exception.message}")
ErrorNotifier.notify(exception, job: self.class.name)
raise # Re-raise to trigger retry
end
end
Concurrency Control (Solid Queue)
class ProcessUserDataJob < ApplicationJob
limits_concurrency key: ->(user_id) { user_id }, duration: 15.minutes
def perform(user_id)
user = User.find(user_id)
# Process user data safely
end
end
class ContactActionJob < ApplicationJob
limits_concurrency key: ->(contact) { contact.id },
duration: 10.minutes,
group: "ContactActions"
end
Scheduling & Delayed Execution
SendReminderJob.perform_later(user) # Immediate
SendReminderJob.set(wait: 1.hour).perform_later(user) # Delayed
SendReminderJob.set(wait_until: Date.tomorrow.noon).perform_later(user) # Scheduled
# Bulk enqueue
ActiveJob.perform_all_later(users.map { |u| SendReminderJob.new(u.id) })
Set ActiveJob::Base.enqueue_after_transaction_commit = true at the application level when jobs depend on records created or changed in the same transaction. That fixes job-before-commit races at the source.
For multi-tenant apps, serialize tenant context at enqueue time and restore it around perform; do not infer the tenant from a bare record ID later.
Anti-Patterns to Avoid
| Anti-Pattern | Problem | Solution |
|---|---|---|
| Fat jobs | Hard to test and maintain | Extract logic to model classes |
| Large serialized payloads | Expensive, stale data | Pass IDs or GlobalID records with clear freshness semantics |
| No retry strategy | Silent failures | Use retry_on with backoff |
| Synchronous calls | Blocks request | Always use perform_later |
| No idempotency | Duplicate processing | Design jobs to be re-runnable |
| Ignoring errors | Silent failures | Use rescue_from with logging |
| Monolithic steps | Can't resume after failure | Use Continuable pattern with state |
Current.user inside perform |
Request context is gone | Pass actor or tenant context at enqueue |
| CPU-heavy or long-transaction job on a fiber queue | Blocks the reactor for every other fiber, or pins a connection for the job's lifetime | Route it to a thread worker queue |
Output Format
When creating or refactoring jobs, provide:
- Job Class - The complete job implementation
- Queue Strategy - Recommended queue and priority
- Error Handling - Retry and failure strategies
- Testing - Example test cases
- Considerations - Concurrency, idempotency notes