Rails Solid Queue: Migreren van Sidekiq naar de native background job-backend van Rails 8
Rails Solid Queue in productie: setup, concurrency-controls, recurring jobs en een stapsgewijze migratiegids weg van Sidekiq zonder één job te verliezen.
Vorige maand hielp ik een SaaS-klant hun Redis-cluster te verwijderen. Niet downgraden, niet resizen — weggooien. Het enige wat het nog deed was Sidekiq draaien. Zodra we hun background jobs naar Rails Solid Queue verplaatsten, verdampte de laatste reden om Redis nog te laten draaien. Hun AWS-rekening zakte met $180 per maand, hun runbook werd een pagina korter, en hun on-call rotatie werd om 3 uur ‘s nachts niet meer gepiept voor Redis-evictions.
Als je mijn Solid Cache-artikel hebt gelezen, weet je waar dit heen gaat. Rails 8 leverde Solid Queue als de default ActiveJob-backend om precies dezelfde reden waarom het Solid Cache leverde: de meeste Rails-apps hebben geen Redis nodig. Ze hebben alleen een plek nodig om een job durable weg te schrijven en een worker die die oppakt. Postgres kan dat prima.
Na negentien jaar Rails heb ik Delayed Job, Resque, Sidekiq, GoodJob en nu Solid Queue in productie zien draaien. Solid Queue is de eerste uit die reeks die ik standaard zou pakken voor een greenfield Rails 8-app — en de eerste waarnaar ik een bestaande Sidekiq-omgeving zou migreren zonder ineen te krimpen.
Wat Rails Solid Queue eigenlijk is
Rails Solid Queue is een database-backed ActiveJob-adapter, onderhouden door 37signals als onderdeel van de “Solid Trifecta” (Solid Cache, Solid Queue, Solid Cable). Het slaat jobs op als rijen in een set tabellen, gebruikt FOR UPDATE SKIP LOCKED om werk op te pakken zonder lock-contention, en draait een supervisor-process dat pools van workers, een scheduler en een dispatcher opspint.
Drie dingen maken het een serieuze Sidekiq-vervanger in plaats van “database-backed jobs, alweer”:
FOR UPDATE SKIP LOCKED— dezelfde Postgres-primitive die queues zoals Que snel maakt. Workers vechten niet om dezelfde rij. Eén SELECT locked een rij en gaat verder, anderen slaan hem over.- Concurrency-controls out of the box — je kunt “slechts één instantie van deze job per user tegelijk” uitdrukken als één regel in de job-klasse. Geen Redis-gebaseerde mutex-gem, geen lua-scripts, geen verlopen locks.
- Recurring jobs — cron-achtige scheduling zit ingebouwd via
config/recurring.yml. Je hebt geensidekiq-cron,wheneverof een aparte scheduler-container nodig.
Het performance-plafond ligt lager dan bij Sidekiq. Een goed getunede Sidekiq-worker knalt 5.000-10.000 triviale jobs per seconde per process weg. Een goed getunede Solid Queue-worker op Postgres blijft dichter bij 1.000-2.000. Voor de meeste Rails-apps is dit betekenisloos — je draait tientallen tot honderden jobs per seconde, geen duizenden. Voor de apps waar het wél telt (payment processors, high-fanout notificatie-pipelines), hou Sidekiq. Voor iedereen anders is Rails Solid Queue de juiste default.
Solid Queue vs Sidekiq: wanneer migreren
Voor we verder gaan, de eerlijke trade-off-tabel die ik met klanten deel:
| Aandachtspunt | Sidekiq | Rails Solid Queue |
|---|---|---|
| Throughput-plafond | Zeer hoog (Redis in-memory) | Hoog genoeg voor ~99% van Rails-apps |
| Operationele oppervlakte | Redis + Sidekiq | Alleen Postgres |
| Durability van jobs | Afhankelijk van Redis-persistence-config | ACID by design |
| Concurrency-locks | Sidekiq Pro / sidekiq-unique-jobs |
Ingebouwd |
| Recurring jobs | sidekiq-cron (Pro) |
Ingebouwd via YAML |
| Web-UI | Uitstekend (sidekiq/web) |
Mission Control (aparte gem) |
| Licentiekosten | Sidekiq Pro/Enterprise zijn betaald | Gratis, MIT |
Migreer naar Solid Queue wanneer Redis er alleen nog is voor Sidekiq. Houd Sidekiq aan wanneer je meer dan een paar duizend jobs per seconde pushet, wanneer je afhankelijk bent van Sidekiq Enterprise-features zoals rate limiters of batches, of wanneer Redis al load-bearing is voor cache en Pub/Sub op manieren die je niet kunt terugdraaien.
Rails Solid Queue installeren op een nieuwe app
Rails 8 genereert nieuwe apps met Solid Queue als default queue-adapter. config/environments/production.rb bevat:
config.active_job.queue_adapter = :solid_queue
config.solid_queue.connects_to = { database: { writing: :queue } }
Solid Queue verwacht zijn eigen databaseconnectie. Voeg die toe aan config/database.yml:
production:
primary:
<<: *default
database: myapp_production
cache:
<<: *default
database: myapp_production_cache
migrations_paths: db/cache_migrate
queue:
<<: *default
database: myapp_production_queue
migrations_paths: db/queue_migrate
Installeer het schema:
bin/rails db:create
bin/rails solid_queue:install:migrations
bin/rails db:migrate
De migraties maken zeven tabellen aan: solid_queue_jobs, solid_queue_ready_executions, solid_queue_claimed_executions, solid_queue_scheduled_executions, solid_queue_failed_executions, solid_queue_pauses en solid_queue_processes. Laat je niet afschrikken door het aantal — elke tabel heeft één saai, duidelijk doel. Degene waar je het meest tegenaan zult query’en is solid_queue_failed_executions wanneer je aan het debuggen bent.
Rails Solid Queue in productie draaien
Solid Queue draait standaard niet binnen je Puma-process. Je draait het als een eigen long-lived process, gesuperviseerd op dezelfde manier waarop je Puma superviseert. bin/jobs wordt voor je gegenereerd:
bin/jobs start
Voor productie definieert config/queue.yml de worker-layout:
production:
dispatchers:
- polling_interval: 1
batch_size: 500
workers:
- queues: [critical, default]
threads: 5
processes: 2
polling_interval: 0.1
- queues: [low, mailers]
threads: 3
processes: 1
polling_interval: 1
Ik draai bewust twee worker-pools. De critical, default-pool heeft korte polling-intervals en meerdere processes zodat betalende klanten niet achter nieuwsbrief-mailings hoeven te wachten. De low, mailers-pool poll’t langzaam en gebruikt minder threads — die jobs mogen vijf seconden wachten zonder dat iemand het merkt.
Onder Kamal voeg je een rol voor de workers toe zodat je ze samen met de web-app deployt:
# config/deploy.yml
servers:
web:
- 10.0.0.10
jobs:
hosts:
- 10.0.0.10
cmd: bin/jobs
Kamal boot per host een jobs-container, herstart die bij deploys en streamt de logs. Geen aparte Sidekiq systemd-unit, geen supervisord, geen PM2.
Concurrency-controls: de feature die mij overtuigde
De ene feature die maakte dat ik sidekiq-unique-jobs niet meer standaard pak bij nieuwe projecten is de ingebouwde limits_concurrency van Solid Queue.
class ImportSpreadsheetJob < ApplicationJob
queue_as :default
limits_concurrency to: 1, key: ->(user, _file) { user.id }, duration: 30.minutes
def perform(user, file)
Importer.new(user).run(file)
end
end
Er kan slechts één ImportSpreadsheetJob per user.id tegelijk draaien. Als een tweede wordt enqueued terwijl de eerste nog bezig is, blokkeert die (draait niet, faalt niet) totdat de eerste klaar is of de 30 minuten verstrijken. Geen Redis-mutex, geen zombie-locks. De geblokkeerde state wordt opgeslagen in een solid_queue_blocked_executions-rij en vrijgegeven door dezelfde supervisor die de claims afhandelt.
Ik gebruik dit constant voor import-jobs, PDF-generatoren en elke externe API met een per-account rate limit. Het is een van de stukken van Rails Solid Queue waar het echt de Sidekiq-community verslaat — met Sidekiq zou je een betaalde of derde-partij-gem installeren en hopen dat de lock-semantiek matcht wat je dacht.
Recurring jobs zonder cron
config/recurring.yml vervangt sidekiq-cron, whenever of een aparte scheduler-service:
production:
cleanup_expired_sessions:
class: CleanupExpiredSessionsJob
schedule: every hour
send_daily_digest:
class: SendDailyDigestJob
args: ["marketing"]
schedule: "0 8 * * *"
refresh_analytics_cache:
class: RefreshAnalyticsCacheJob
schedule: every 15 minutes
queue: low
Het scheduler-process (een van de dispatchers in config/queue.yml) leest dit bestand, insert scheduled executions op de juiste tijden en workers pakken ze op als elke andere job. Omdat de schedule-state in Postgres leeft, vuur je niet dubbel bij een deploy of scale-out — een verse scheduler ziet “ik heb de digest-job van 8:00 al ingepland” en gaat verder.
Voor alles waar ik in 2019 een Rake-task en een cron-regel voor zou hebben geschreven, schrijf ik nu een job en één YAML-blok. Rollbacks omvatten de schedule; audit logs omvatten de schedule; de schedule wordt code-reviewed. Dit alleen al rechtvaardigt de migratie voor teams die een crontab tussen productie-servers hadden zien wegdrijven.
Stap voor stap: migreren van Sidekiq naar Rails Solid Queue
Hier is het playbook dat ik daadwerkelijk volg wanneer ik een Sidekiq-omgeving naar Solid Queue verhuis. Doel: een zero-loss cutover met op elk moment de mogelijkheid tot rollback.
1. Installeer Solid Queue naast Sidekiq
# Gemfile
gem "sidekiq" # voorlopig laten staan
gem "solid_queue"
gem "mission_control-jobs" # web-UI
Draai de installer:
bin/rails solid_queue:install
bin/rails db:migrate
Wissel de adapter nog niet. Beide backends bestaan naast elkaar.
2. Verhuis nieuwe jobs klasse-per-klasse
ActiveJob staat een per-klasse queue-adapter toe:
class LowRiskReportJob < ApplicationJob
self.queue_adapter = :solid_queue
queue_as :low
def perform(report_id)
Report.find(report_id).generate!
end
end
Begin met jobs die idempotent, low-frequency en low-risk zijn. Rapportages, wekelijkse digests, cleanup-taken. Als Solid Queue om welke reden dan ook stukgaat, worden alleen deze jobs geraakt — de rest van je queue draait nog op Sidekiq.
Deploy. Kijk naar Mission Control (/jobs) en je APM. Geef het een week.
3. Verhuis recurring jobs naar config/recurring.yml
Verwijder de equivalente entries uit je sidekiq-cron-config. Zet ze in config/recurring.yml. Dit is de stap met de hoogste opbrengst: het haalt een hele klasse “hebben we dit wel goed ingepland?”-onzekerheid weg.
4. Zet de default adapter om
Zodra een representatief deel van je jobs een week zonder incidenten op Solid Queue draait, zet de globale default om:
# config/environments/production.rb
config.active_job.queue_adapter = :solid_queue
Elke job die nog Sidekiq nodig heeft (high-throughput, gebruikt een Sidekiq-specifieke feature) zet expliciet self.queue_adapter = :sidekiq. Alles anders verhuist mee.
5. Drain en verwijder Sidekiq
Na de omschakeling houdt Sidekiq nog jobs vast die vóór de deploy geënqueued waren. Laat Sidekiq draineren — verwijder de Redis-keys niet. Zodra Sidekiq::Queue.all.map(&:size).sum nul is en Sidekiq::RetrySet.new.size nul is, haal je de gem en de Redis-afhankelijkheid weg:
# Gemfile
# gem "sidekiq" # weg
Verwijder config/sidekiq.yml, de Kamal sidekiq-rol en de Redis env var. Als Redis er alleen was voor Sidekiq (zoals bij mijn klant hierboven), verwijder je de Redis-cluster.
Rails Solid Queue monitoren in productie
Mission Control geeft je een web-UI die grotendeels doet wat sidekiq/web deed — queue-diepte, gefaalde jobs, retries, pauses, individuele job-inspectie:
# config/routes.rb
authenticate :user, ->(u) { u.admin? } do
mount MissionControl::Jobs::Engine, at: "/jobs"
end
Naast de UI horen drie metrics op je dashboard te staan:
# Queue-diepte per queue
SolidQueue::Job.where(finished_at: nil).group(:queue_name).count
# Aantal gefaalde jobs
SolidQueue::FailedExecution.count
# Leeftijd van de oudste ready job (kanarie voor worker-uithongering)
oldest = SolidQueue::ReadyExecution.minimum(:created_at)
Time.current - oldest if oldest
Ship die naar Datadog, Grafana of wat je ook gebruikt. De leeftijd van de oudste ready job is de meest bruikbare metric — die vertelt je of workers bijblijven op een manier die queue-diepte alleen niet doet. Een queue met 10.000 jobs en een oudste leeftijd van 2 seconden is gezond. Een queue met 50 jobs en een oudste leeftijd van 4 minuten heeft een vastgelopen worker.
Voor alerting werkt Sentry uitstekend met Solid Queue — configureer Rails.error.subscribe en elke job-failure komt in Sentry terecht met de arguments, backtrace en retry-count meegestuurd.
Valkuilen die ik echt tegengekomen ben
De queue-database niet op een eigen connection pool zetten. Solid Queue kan veel connection-churn genereren. Als hij een pool deelt met je web-workers, gaan Puma-requests blocken op de pool. Gebruik connects_to met een aparte database.
Vergeten om bin/jobs in development te starten. Rails 8 start Solid Queue niet automatisch tijdens bin/dev. Voeg een worker-regel toe aan je Procfile.dev of accepteer dat jobs tijdens lokale ontwikkeling in de queue blijven staan. Ik voeg meestal toe:
worker: bin/jobs
polling_interval te agressief zetten. Een polling-interval van 0.01 seconden op acht worker-threads is 800 SELECTs per seconde op je Postgres, terwijl er niks gebeurt behalve “is er werk?”. Begin op 0.1 voor hete queues en 1.0 voor koude. De dispatcher wekt workers via LISTEN/NOTIFY wanneer nieuwe jobs binnenkomen, dus polling is een fallback, niet het primaire pad.
Unique-jobs-semantiek verkeerd migreren. limits_concurrency op Solid Queue blokkeert; sidekiq-unique-jobs gooit typisch weg. Lees je bestaande lock-configuratie zorgvuldig voordat je verhuist — een job die onder Sidekiq stilletjes werd gedropt, blokkeert nu een worker-thread voor de volledige duur als je de config naïef vertaalt.
FAQ
Is Rails Solid Queue production-ready?
Ja. 37signals draait HEY en Basecamp al meer dan een jaar op Solid Queue. Rails 8 levert het niet voor niks als default ActiveJob-adapter. Voor workloads onder een paar duizend jobs per seconde, op een fatsoenlijk geformatte Postgres, is het vandaag production-ready.
Heb ik een aparte database nodig voor Solid Queue?
Niet vereist, maar sterk aanbevolen. Solid Queue is write-heavy (elke job-insert, elke claim, elke finish is een write). Op je primary application-database is prima voor kleine apps maar zal op schaal in je write-IOPS opduiken. Een aparte database op dezelfde Postgres-cluster is de sweet spot voor de meesten.
Hoe verhoudt Rails Solid Queue zich tot GoodJob?
Beide zijn Postgres-backed, beide gebruiken SKIP LOCKED, beide zijn uitstekend. GoodJob is volwassener en heeft een rijkere web-UI; Solid Queue is de officieel-gezegende Rails 8-default en heeft het concurrency-controls-en-recurring-jobs-verhaal ingebouwd. Voor een nieuwe Rails 8-app zou ik Solid Queue kiezen voor de alignment met de richting van de Rails-core-team. Voor een bestaande GoodJob-deployment is er geen dringende reden om over te stappen.
Kan ik Rails Solid Queue gebruiken met MySQL?
Ja. Solid Queue ondersteunt MySQL 8+ en SQLite naast Postgres. SKIP LOCKED zit sinds 8.0 in MySQL. De throughput is vergelijkbaar. Op SQLite is het bedoeld voor development en kleine single-server-deployments — combineer het met Litestream voor durability als je die kant op gaat.
Migreren van Sidekiq naar Solid Queue, of background jobs opzetten op een nieuwe Rails 8-app? TTB Software is gespecialiseerd in Rails-architectuur, upgrades en productie-DevOps. We doen dit al negentien jaar.
Related Articles
Ledenboek bouwen: verenigingsbestuur vastleggen in Rails
Hoe we een ledenadministratieplatform voor Nederlandse verenigingen bouwden, en waarom het moeilijke deel niet de CRU...
Euromailing bouwen: waarom we onze eigen MTA draaien in plaats van een ESP door te verkopen
Hoe we een AVG-native e-mailmarketingplatform bouwden op Rails 8.1 en KumoMTA, en waarom het bezit van de verzendlaag...
Rails 2FA (TOTP): Twee-factor-authenticatie met ROTP, back-upcodes en versleutelde secrets
Rails 2FA met TOTP: implementeer twee-factor-authenticatie met ROTP, genereer back-upcodes, versleutel secrets met Ac...