Skip to main content
Everything that doesn’t happen during a request runs as a background job: sending emails, processing images, delivering webhooks, importing and exporting catalogs, releasing expired stock reservations. Spree uses Active Job (Rails’ standard job interface), so the queue backend is swappable — but the default needs nothing beyond your database.

Solid Queue — the default

Production uses Solid Queue — the built-in background job processor: jobs are stored in your database alongside everything else. There is nothing extra to provision, and by default the job supervisor runs inside the web container — one container is the whole application (combined mode). When job volume grows, switch to split mode: run a dedicated worker from the same image with bin/jobs, and set SOLID_QUEUE_IN_PUMA=false on the web service. Multiple web instances and multiple workers are safe by default — they coordinate through the database.

Job Dashboard

Every deployment ships Mission Control — a web UI for background jobs — at /jobs: inspect, retry, and discard jobs from the browser. It’s guarded by HTTP Basic auth: set MISSION_CONTROL_USER and MISSION_CONTROL_PASSWORD in production (without them the dashboard stays locked).

Recurring Jobs

Scheduled work is defined in config/recurring.yml, Solid Queue’s built-in cron. A new project already schedules everything Spree needs: The marketplace jobs do nothing on a store with no sellers, so keep them enabled. If your project predates any of these, copy the missing entries from the starter’s config/recurring.yml. Add your own:
config/recurring.yml

Marketplace payouts

A marketplace settles its sellers on a recurring job, which a new project already schedules:
config/recurring.yml
Run it daily whatever intervals your sellers are on. The job only fans out; whether a given seller is due — and when they were last settled — is decided per seller, because a cron expression cannot say “weekly, counted from that seller’s own last payout”. A seller whose interval is manual is skipped here by design and is settled by hand from the operator dashboard.

Queues

config/queue.yml lists queues in polling order: checkout-critical and operator-facing work first, bulk catalog processing second, housekeeping last. A trailing "*" catches any queue added later. Large CSV imports are additionally capped so they can’t occupy the whole worker pool — see SPREE_IMPORT_JOB_CONCURRENCY.

Swapping to Sidekiq

For very high job volume, Spree works with Sidekiq on Redis or Valkey — like any Active Job backend. This is a bigger step than the cache swap; here is the full setup. Add the gems and switch the adapter:
Configure the queues with weights — Sidekiq’s equivalent of Solid Queue’s polling order (checkout-critical work first, bulk catalog processing second, housekeeping last):
config/sidekiq.yml
Unlike Solid Queue, Sidekiq has no catch-all queue: a job on a queue missing from this list never runs. Every queue name you assign with Spree.queues in config/initializers/spree.rb must appear here. Any Spree.queues entry you don’t assign uses default. Move the config/recurring.yml schedules to sidekiq-cron (it auto-loads config/schedule.yml):
config/schedule.yml
sidekiq-cron schedules jobs, not commands, so price history pruning needs a small job that runs the task:
app/jobs/prune_price_history_job.rb
The /jobs dashboard is Solid Queue-specific — mount Sidekiq’s Web UI instead:
Finally: point Sidekiq at your Redis/Valkey via REDIS_URL, run bundle exec sidekiq as a dedicated worker service (set RAILS_MAX_THREADS to at least SIDEKIQ_CONCURRENCY there so the database pool covers every worker thread), and remove the SOLID_QUEUE_IN_PUMA setting — with Sidekiq, web and worker are always separate processes. Reach for this when a dedicated Solid Queue worker with raised JOB_THREADS/JOB_CONCURRENCY no longer keeps up — for most stores, that point never arrives.