March 2, 2026 · 12 min read
How to Set Up Background Jobs in Rails 8 with Solid Queue (No Redis Required)
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.ymland 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.