Omaship

March 2, 2026 · 12 min read

How to Set Up Background Jobs in Rails 8 with Solid Queue (No Redis Required)

Jeronim Morina

Jeronim Morina

Founder, Omaship

Every SaaS needs background jobs. Sending emails, processing webhooks, running reports, cleaning up stale data -- these tasks do not belong in a web request. Rails 8 ships with Solid Queue as the default background job backend, and it changes the economics of running a SaaS entirely: no Redis, no extra infrastructure, no monthly bill for a managed queue service.

For years, Sidekiq + Redis was the Rails community's default answer to background processing. It worked well. It still works well. But it introduced an external dependency that every SaaS founder had to provision, monitor, pay for, and keep running. For solo founders deploying to a single VPS, that Redis instance was often the difference between a $5/month server and a $25/month stack.

Solid Queue eliminates that dependency. It stores jobs in your existing database -- the same SQLite or PostgreSQL instance your app already uses. No new moving parts. No new failure modes. No new monthly costs.

What Solid Queue actually is

Solid Queue is a database-backed Active Job adapter built by the Rails core team. It replaces Sidekiq, Delayed Job, Good Job, and every other queue backend you have used before. Here is what makes it different:

  • Database-backed: Jobs are stored in your database, not in Redis. This means your jobs participate in the same ACID transactions as your application data. Enqueue a job inside a transaction, and it only becomes visible when the transaction commits.
  • Multi-database aware: Rails 8 configures Solid Queue in a separate database by default (queue.sqlite3). This keeps job tables out of your primary database and prevents queue churn from affecting your application queries.
  • Concurrency controls: Built-in semaphore-based concurrency limiting. Limit how many instances of a job run simultaneously -- essential for API rate limiting, payment processing, and resource-intensive tasks.
  • Recurring jobs: Cron-style scheduling without an extra gem. Define recurring jobs in config/recurring.yml and Solid Queue runs them on schedule.
  • Mission Control UI: A mountable Rails engine that gives you a web dashboard to inspect queues, retry failed jobs, and monitor throughput. No separate Sidekiq Web dependency.
  • Pausable queues: Pause and resume individual queues without restarting workers. Useful for maintenance windows or when a downstream service is degraded.

Setting up Solid Queue in a new Rails 8 app

If you start a new Rails 8 application, Solid Queue is already configured. Here is what the generator sets up for you:

# config/database.yml (Rails 8 default)

production:
  primary:
    <<: *default
    database: storage/production.sqlite3
  queue:
    <<: *default
    database: storage/production_queue.sqlite3
    migrations_paths: db/queue_migrate

The queue database is separate from your primary database. This is intentional: Solid Queue performs frequent polling and cleanup operations that would add noise to your primary database. Keeping them separate means your application queries stay fast.

# config/environments/production.rb

config.active_job.queue_adapter = :solid_queue
config.solid_queue.connects_to = { database: { writing: :queue } }

That is the entire configuration. Two lines in your production config, a database entry, and you have a production-ready job queue. Compare this to Sidekiq: install the gem, provision Redis, configure the connection, set up a Sidekiq process in your Procfile, and add monitoring.

The five background jobs every SaaS needs

Every SaaS application needs at least these five categories of background work. Here is how to implement each one with Solid Queue:

1. Transactional emails

Welcome emails, password resets, invoice receipts -- these must be sent reliably but do not need to block the user's request. Rails makes this trivially easy:

# app/mailers/user_mailer.rb
class UserMailer < ApplicationMailer
  def welcome(user)
    @user = user
    mail(to: @user.email_address, subject: "Welcome to #{app_name}")
  end
end

# In your controller or model:
UserMailer.welcome(user).deliver_later

deliver_later enqueues the email as a background job via Active Job. With Solid Queue, this job is stored in your queue database and processed by the next available worker. If the mail server is temporarily down, the job retries automatically.

2. Webhook processing

Payment webhooks from Stripe, deployment notifications from GitHub, status updates from third-party APIs -- these arrive asynchronously and must be processed reliably. The pattern is straightforward: acknowledge the webhook immediately, then process it in the background.

# app/jobs/process_stripe_webhook_job.rb
class ProcessStripeWebhookJob < ApplicationJob
  queue_as :webhooks
  retry_on Stripe::APIConnectionError, wait: :polynomially_longer, attempts: 5

  def perform(payload, signature)
    event = Stripe::Webhook.construct_event(
      payload, signature, Rails.application.credentials.stripe[:webhook_secret]
    )

    case event.type
    when "checkout.session.completed"
      handle_checkout_completed(event.data.object)
    when "customer.subscription.updated"
      handle_subscription_updated(event.data.object)
    when "invoice.payment_failed"
      handle_payment_failed(event.data.object)
    end
  end
end

Notice the retry_on declaration. Solid Queue respects all Active Job retry semantics. If the Stripe API is temporarily unreachable, the job backs off polynomially and retries up to five times. No custom retry logic needed.

3. Data cleanup and maintenance

Expired sessions, orphaned uploads, old audit logs -- every SaaS accumulates data that needs periodic cleanup. Solid Queue's recurring jobs handle this without a separate scheduling gem:

# config/recurring.yml

production:
  cleanup_expired_sessions:
    class: CleanupExpiredSessionsJob
    schedule: every day at 3am UTC
  purge_old_logs:
    class: PurgeOldLogsJob
    schedule: every Sunday at 2am UTC
    args:
      - 90
# app/jobs/cleanup_expired_sessions_job.rb
class CleanupExpiredSessionsJob < ApplicationJob
  queue_as :maintenance

  def perform
    Session.where("updated_at < ?", 30.days.ago).delete_all
  end
end

No whenever gem. No system crontab to manage. No separate scheduler process. Solid Queue handles recurring jobs natively. Define them in YAML, and they run on schedule.

4. Report generation

Monthly usage reports, analytics exports, invoice PDFs -- these are compute-intensive tasks that should never run in a web request. The concurrency control feature in Solid Queue is particularly useful here:

# app/jobs/generate_monthly_report_job.rb
class GenerateMonthlyReportJob < ApplicationJob
  queue_as :reports
  limits_concurrency to: 2, key: ->(account_id) { "report-#{account_id}" }

  def perform(account_id, month)
    account = Account.find(account_id)
    report = ReportGenerator.new(account, month).generate
    ReportMailer.monthly_report(account, report).deliver_later
  end
end

The limits_concurrency declaration ensures at most two report jobs run simultaneously per account. This prevents a single account from consuming all your worker capacity and protects your database from concurrent heavy queries.

5. Third-party API synchronization

Syncing data with external services -- CRM updates, analytics pushes, inventory checks -- requires careful rate limiting. Solid Queue's concurrency controls handle this elegantly:

# app/jobs/sync_crm_contact_job.rb
class SyncCrmContactJob < ApplicationJob
  queue_as :integrations
  limits_concurrency to: 5, key: -> { "crm-api" }
  retry_on Faraday::TooManyRequestsError, wait: 30.seconds, attempts: 3

  def perform(user_id)
    user = User.find(user_id)
    CrmClient.new.upsert_contact(
      email: user.email_address,
      name: user.name,
      plan: user.subscription&.plan_name
    )
  end
end

At most five CRM sync jobs run at the same time across your entire fleet. If the API returns a 429 (rate limited), the job waits 30 seconds and retries. This kind of coordination used to require a separate rate-limiting library. With Solid Queue, it is a single line.

Queue configuration for production

A production SaaS needs multiple queues with different priorities. Here is a configuration that works well for most applications:

# config/solid_queue.yml

production:
  dispatchers:
    - polling_interval: 1
      batch_size: 500
  workers:
    - queues: "default,mailers,webhooks"
      threads: 5
      processes: 2
      polling_interval: 0.1
    - queues: "reports,maintenance,integrations"
      threads: 3
      processes: 1
      polling_interval: 1

This configuration runs two groups of workers. The first group handles high-priority work (default queue, emails, webhooks) with more threads and faster polling. The second group handles lower-priority work (reports, maintenance, integrations) with fewer threads and slower polling. Both run in the same process alongside your web server -- no separate Procfile entry needed when using Puma's plugin.

# config/puma.rb

plugin :solid_queue

One line. Puma starts Solid Queue workers alongside your web processes. On a single $5 VPS, this means your web server and background workers share the same process tree. No separate worker dyno. No extra container. No additional cost.

Solid Queue vs Sidekiq: the honest comparison

Sidekiq is battle-tested and powerful. If you already run it in production, there is no urgent reason to migrate. But for new projects in 2026, the calculus has changed.

Solid Queue Sidekiq
External dependency None (database-backed) Redis required
Transactional integrity Jobs enqueued within DB transactions Jobs visible immediately (race condition risk)
Recurring jobs Built-in (recurring.yml) Requires sidekiq-cron or sidekiq-scheduler
Concurrency controls Built-in semaphore Sidekiq Enterprise ($179/mo) or custom
Monitoring UI Mission Control (free, mountable) Sidekiq Web (included) or Sidekiq Pro
Cost (infrastructure) $0 additional $7-25/mo for managed Redis
Raw throughput Good (thousands/min on SQLite) Excellent (millions/min with Redis)
Best for Most SaaS apps (< 10K jobs/min) High-throughput, Redis-heavy architectures

The throughput difference rarely matters for SaaS applications. A typical SaaS processes hundreds of jobs per hour, not millions per minute. Solid Queue handles this with ease. If you are building a SaaS that processes millions of jobs per minute, you have bigger architectural decisions to make than which queue backend to use.

Error handling and observability

Jobs fail. APIs go down, databases lock, third-party services return unexpected responses. Solid Queue's error handling follows Active Job conventions, which means you already know how to use it:

# app/jobs/application_job.rb
class ApplicationJob < ActiveJob::Base
  # Automatically retry transient failures
  retry_on ActiveRecord::Deadlocked, wait: 5.seconds, attempts: 3
  retry_on Net::OpenTimeout, wait: :polynomially_longer, attempts: 5

  # Discard jobs that will never succeed
  discard_on ActiveJob::DeserializationError

  # Log and report all failures
  after_discard do |job, error|
    Rails.logger.error("[JOB DISCARDED] #{job.class.name}: #{error.message}")
    ErrorReporter.report(error, context: { job: job.class.name, arguments: job.arguments })
  end
end

Define your retry and discard policies in ApplicationJob, and every job in your application inherits them. Override in specific jobs when you need different behavior. This is standard Active Job -- nothing Solid Queue specific -- which means switching backends later (if you ever need to) requires zero changes to your job classes.

Monitoring with Mission Control

Solid Queue ships with Mission Control, a mountable Rails engine that gives you visibility into your job queue:

# Gemfile

gem "mission_control-jobs"

# config/routes.rb

authenticate :user, ->(user) { user.admin? } do
  mount MissionControl::Jobs::Engine, at: "/jobs"
end

Navigate to /jobs and you can see all queues, pending jobs, failed jobs, and recurring job schedules. Retry failed jobs with a click. Pause queues. Inspect job arguments. All without leaving your Rails application.

The Solid Trifecta: Queue, Cache, and Cable

Solid Queue is part of what the Rails community calls the "Solid Trifecta" -- three database-backed libraries that replace Redis for the most common use cases:

  • Solid Queue: Background jobs (replaces Sidekiq + Redis)
  • Solid Cache: Application caching (replaces Redis as a cache store)
  • Solid Cable: WebSocket connections via Action Cable (replaces Redis pub/sub)

Together, they mean a Rails 8 application has zero external dependencies beyond the database. Your entire stack -- web server, background jobs, caching, real-time updates -- runs on a single process with SQLite. This is not a toy setup. Basecamp and HEY run on this stack in production.

For SaaS founders, this changes the economics fundamentally. Your production infrastructure is:

  • One VPS (Hetzner, DigitalOcean, or any provider) -- $5-10/month
  • One process (Puma with Solid Queue plugin)
  • One database (SQLite, or PostgreSQL if you prefer)
  • One deployment tool (Kamal)

No Redis to provision. No separate worker process to manage. No message broker to configure. The simplicity is not a compromise -- it is an advantage. Fewer moving parts means fewer things that break at 3 AM.

When to not use Solid Queue

Solid Queue is the right choice for most SaaS applications. But there are scenarios where you should consider alternatives:

  • Extreme throughput: If you genuinely process millions of jobs per minute, Redis-backed queues like Sidekiq are faster. This is rare for SaaS -- most applications are nowhere near this threshold.
  • Existing Redis infrastructure: If your stack already includes Redis for caching and pub/sub, adding Sidekiq has near-zero marginal cost. Switching to Solid Queue would not save you anything.
  • Complex workflow orchestration: If you need multi-step job pipelines with branching logic, tools like Temporal or Karafka might be more appropriate. Solid Queue handles individual jobs well but does not have built-in workflow orchestration.

For everything else -- which covers 95% of SaaS applications -- Solid Queue is the pragmatic choice. It is simpler, cheaper, and maintained by the Rails core team.

Migration checklist: from Sidekiq to Solid Queue

If you are running Sidekiq and want to migrate, here is the step-by-step process:

Step Action Time
1 Add solid_queue gem and run bin/rails solid_queue:install 5 min
2 Replace Sidekiq-specific APIs (SomeWorker.perform_async) with Active Job (SomeJob.perform_later) 1-4 hrs
3 Convert sidekiq-cron entries to config/recurring.yml 15 min
4 Replace Sidekiq middleware with Active Job callbacks or around_perform 30 min
5 Switch config.active_job.queue_adapter to :solid_queue 1 min
6 Add Puma plugin (plugin :solid_queue) or separate process 1 min
7 Deploy to staging, run all jobs, verify behavior 1-2 hrs
8 Remove sidekiq, redis gems and decommission Redis 10 min

The hardest part is step 2: replacing Sidekiq-specific APIs. If you already use Active Job as an abstraction layer (perform_later instead of perform_async), this step is trivial. If you used Sidekiq's native API directly, you will need to refactor those calls.

Testing background jobs

Testing jobs with Solid Queue follows standard Active Job testing patterns. Rails provides test helpers that make this straightforward:

# test/jobs/process_stripe_webhook_job_test.rb
class ProcessStripeWebhookJobTest < ActiveJob::TestCase
  test "processes checkout.session.completed event" do
    payload = build_stripe_event("checkout.session.completed")

    assert_changes -> { user.reload.subscribed? }, from: false, to: true do
      ProcessStripeWebhookJob.perform_now(payload, valid_signature)
    end
  end

  test "retries on API connection errors" do
    assert_enqueued_with(job: ProcessStripeWebhookJob) do
      ProcessStripeWebhookJob.perform_later(payload, signature)
    end
  end

  test "enqueues email after successful webhook processing" do
    assert_enqueued_emails 1 do
      ProcessStripeWebhookJob.perform_now(payload, valid_signature)
    end
  end
end

Notice that none of this code references Solid Queue. It uses Active Job's test helpers: assert_enqueued_with, perform_now, assert_enqueued_emails. Your tests are backend-agnostic. Switch from Solid Queue to Sidekiq or any other backend, and your tests still pass without changes.

The bottom line

Background jobs used to require Redis, a separate process, and a monthly infrastructure bill. Rails 8 with Solid Queue eliminates all three. You get a production-ready job queue that runs in your existing database, starts with your web server, and costs nothing beyond what you already pay for hosting.

For SaaS founders in 2026, the question is not whether to use background jobs -- it is why you would add Redis to your stack when you do not have to. Solid Queue gives you transactional integrity, concurrency controls, recurring jobs, and a monitoring UI with zero additional infrastructure. Ship simpler. Ship cheaper. Ship with fewer things that break.

Define your jobs with Active Job. Configure your queues in YAML. Add one line to Puma. Deploy. That is the entire setup. Your $5 VPS now handles web requests, background processing, caching, and real-time updates -- all from a single process. That is the Rails 8 promise, and Solid Queue delivers it.

Ship with Solid Queue from day one

Omaship configures Solid Queue, Solid Cache, and Solid Cable out of the box. Background jobs, caching, and WebSockets -- all running on your database with zero external dependencies. Production-ready from the first deploy.

Continue reading

We use analytics and session recordings to learn which parts of Omaship help and which need work. Accept all, or customize what you share.

Privacy policy