Background Jobs with Solid Queue in Rails

Background Jobs with Solid Queue in Rails

Every Rails app eventually needs work that should not block a web request: emails, webhook delivery, PDF generation, imports. Solid Queue - the default Active Job backend in modern Rails - keeps that work in your database instead of requiring Redis on day one.

Why Solid Queue Fits Many Apps

Sidekiq and Redis are still excellent. Solid Queue wins when you want:

  • Fewer moving parts in staging and early production
  • Jobs that survive with the same Postgres (or MySQL) you already run
  • First-class Active Job APIs you already know (perform_later, retries, queues)

If you already operate Redis well and need extreme throughput, Sidekiq remains a strong choice. For a large share of SaaS and internal apps, Solid Queue is enough.

Basic Setup

With Rails 8 defaults, Solid Queue is close to ready. Typical pieces:

# config/application.rb / environment
config.active_job.queue_adapter = :solid_queue
# config/queue.yml (simplified)
default: &default
  dispatchers:
    - polling_interval: 1
      batch_size: 500
  workers:
    - queues: "*"
      threads: 3
      processes: <%= ENV.fetch("JOB_CONCURRENCY", 1) %>
      polling_interval: 0.1

Run the worker process in production (Kamal, systemd, or your platform's process model) alongside Puma - jobs do not run inside the web process by default.

Design Jobs Like Public APIs

Treat each job as a small, idempotent unit of work:

class DeliverWebhookJob < ApplicationJob
  queue_as :default
  retry_on Net::OpenTimeout, wait: :polynomially_longer, attempts: 5

  def perform(delivery_id)
    delivery = WebhookDelivery.find(delivery_id)
    return if delivery.completed?

    Webhooks::Deliver.call(delivery)
  end
end

Guidelines that save pain later:

  • Pass IDs, not giant Active Record objects
  • Make perform safe to run twice (idempotent)
  • Use retry_on / discard_on deliberately - don't retry forever on bad data
  • Keep web requests thin: enqueue, then return

Queues and Priorities

Not all jobs are equal. Split noisy work from user-facing work:

class SendReceiptEmailJob < ApplicationJob
  queue_as :mailers
end

class RebuildSearchIndexJob < ApplicationJob
  queue_as :low
end

Configure workers so :mailers and :default get more threads or dedicated processes than :low. A stuck import should not delay password-reset emails.

Failures, Retries, and Visibility

Jobs fail. Plan for it:

  • Retries - transient network errors deserve retries; validation errors usually do not
  • Logging - structured logs with job_id, record id, and queue name
  • Dashboards - Mission Control or your APM so stuck queues are obvious
  • Dead letter habit - inspect discarded jobs weekly; silent discard piles grow

If a job talks to a third-party API, add timeouts and circuit-breaker style guards so one vendor outage does not fill your queue with useless retries.

When to Keep Redis / Sidekiq

Stay on Sidekiq (or similar) when you need:

  • Very high job throughput with tight latency SLAs
  • Advanced unique-job or batch tooling your team already depends on
  • A clear ops investment already centered on Redis

You can also mix: Solid Queue for most work, a specialized backend for a hot path. Don't migrate for fashion - migrate for operational simplicity or scale.

Wrapping Up

Solid Queue gives Rails apps a durable, Active Job-native path for background work without standing up Redis on day one. Keep jobs idempotent, split queues by priority, and watch failures like production traffic.

Planning a Rails upgrade or job-backend change? Contact NekoCoding for architecture help.