Van Sidekiq naar Solid Queue migreren: Zero-downtime gids voor productie Rails-apps
Een SaaS-klant belde me in februari met een probleem dat de meeste Rails-teams de komende twee jaar zullen tegenkomen: hun Sidekiq Pro-licentie kostte vijfcijferig per jaar, hun Redis-node had het vorige kwartaal twee keer een failover gehad, en Rails 8 was uitgekomen met Solid Queue als standaard. Hun CTO wilde weten of een Sidekiq naar Solid Queue migratie haalbaar was voor een systeem dat dertig miljoen jobs per maand verwerkte — en zo ja, hoe dat moest zonder ook maar één betaaljob onderweg te verliezen. We rondden de migratie in zes weken af. Hun Redis-rekening ging naar nul. Sindsdien hebben ze geen enkel queue-gerelateerd incident meer gehad.
Na negentien jaar Rails heb ik veel achtergrondjob-systemen verhuisd. Resque naar Sidekiq. Delayed Job naar Sidekiq. Sidekiq naar GoodJob. En nu, telkens opnieuw, Sidekiq naar Solid Queue. Deze post is het draaiboek dat ik bij klanten gebruik: de migratiestappen, het dual-running-patroon, wat er stuk gaat, hoe je geplande en terugkerende jobs migreert, en het rollback-plan dat je moet schrijven vóór je ook maar één regel Sidekiq-configuratie verwijdert.
Als je mijn eerdere stuk over Solid Queue-fundamenten nog niet hebt gelezen, begin daar. Deze post gaat ervan uit dat je al weet wat Solid Queue is en wil weten hoe je er vanaf Sidekiq komt.
Waarom een Sidekiq naar Solid Queue migratie de moeite waard is
Wees voordat je begint eerlijk over de afwegingen. Solid Queue is een degelijke vervanger, geen strikte upgrade.
Je wint operationele eenvoud: één service minder om te draaien, monitoren, back-uppen en patchen. Je wint transactionele enqueue — Order.create! en ChargeOrderJob.perform_later(order) committen nu atomair in dezelfde Postgres-transactie, wat een hele klasse aan “job draaide voordat de rij bestond”-bugs elimineert. Je raakt een Sidekiq Pro- of Enterprise-rekening kwijt die lineair meegroeit met je verkeer.
Je verliest pure doorvoer. Sidekiq-workers trekken jobs in microseconden uit Redis-lists; Solid Queue-workers doen elke polling-interval een SELECT FOR UPDATE SKIP LOCKED tegen Postgres, wat milliseconden kost. Voor de meeste apps zie je het verschil niet. Voor high-fanout-systemen die vijftigduizend jobs per minuut op één queue duwen, voel je het.
De Sidekiq naar Solid Queue migratie is juist als: Redis een serieus deel van je ops-complexiteit is, je jobvolume onder ongeveer vijftig miljoen jobs per maand zit, je Postgres ruimte heeft (of je bereid bent een aparte queue-database toe te voegen), en je niet zwaar leunt op Sidekiq Pro-features zoals batches of unique jobs zonder directe Solid Queue-tegenhanger.
Het is verkeerd als: je op hyperscale zit, je team uit twee mensen bestaat en Sidekiq prima werkt, of je afhankelijk bent van Sidekiq-batches en die orchestratie helemaal opnieuw zou moeten schrijven.
Pre-migratie audit: wat draait er eigenlijk
De grootste fout die teams maken is beginnen aan de migratie voordat ze begrijpen wat ze hebben. Besteed hier een dag aan.
Open een rails console op productie en inventariseer je jobs:
Sidekiq::Stats.new.queues
# => {"default"=>0, "mailers"=>3, "critical"=>0, "low"=>421}
Sidekiq::Cron::Job.all.map(&:name)
# Elke terugkerende job die je ooit hebt opgezet
Sidekiq::ScheduledSet.new.size
# Jobs gepland voor de toekomst
Sidekiq::RetrySet.new.size
# Jobs die momenteel in de retry-queue staan
Loop de codebase langs voor elke sidekiq_options-aanroep:
git grep -n "sidekiq_options"
git grep -n "Sidekiq::Batch"
git grep -n "sidekiq_retry_in"
git grep -n "include Sidekiq::Job"
Maak een spreadsheet. Noteer per job-klasse: de queue, retry-instellingen, eventuele uniciteit-constraints, batch-lidmaatschap, en Sidekiq-specifieke middleware. Deze lijst is je migratieplan. Alles erin zonder Solid Queue-equivalent moet herschreven worden voordat je kunt overschakelen.
De pijnpunten: unique jobs (Sidekiq Enterprise) — Solid Queue heeft andere concurrency-primitieven. Sidekiq::Batch — geen directe tegenhanger; je modelleert de fan-out/fan-in in je eigen tabellen. sidekiq_options lock: :until_executed — vervang door een ConcurrencyControl of een advisory lock in de job. Ik schreef over advisory locks voor precies dit soort patronen.
Solid Queue naast Sidekiq opzetten
De migratie draait beide systemen tegelijk. Nieuwe jobs gaan stapsgewijs naar Solid Queue terwijl Sidekiq zijn queues leegt. Dit is het veiligste pad dat ik ken.
Voeg de gem toe en installeer Solid Queue in een eigen database:
# Gemfile
gem "solid_queue", "~> 1.1"
gem "mission_control-jobs", "~> 1.0" # Web UI
bundle install
bin/rails generate solid_queue:install
De generator wil Solid Queue in je primaire database installeren. Doe dat niet op een drukke app. Configureer een aparte database in config/database.yml:
production:
primary:
<<: *primary_config
queue:
<<: *primary_config
database: my_app_production_queue
migrations_paths: db/queue_migrate
Dan in config/application.rb:
config.solid_queue.connects_to = { database: { writing: :queue } }
Waarom een aparte database? Job-tabellen worden hard belast. Inserts, updates, deletes, elke seconde. Je wilt niet dat autovacuum op je solid_queue_jobs-tabel vecht om I/O met je echte applicatieverkeer. Ik schreef over autovacuum tunen voor tabellen met hoge churn — pas die instellingen vanaf dag één toe op je queue-database.
Voer de Solid Queue-migraties uit naar de nieuwe database:
bin/rails db:create:queue
bin/rails db:migrate:queue
Je hebt nu Solid Queue-tabellen, maar er gebruikt nog niets ze. Sidekiq doet nog steeds al het werk.
Het dual-running patroon
Dit is het hart van de Sidekiq naar Solid Queue migratie. Je wilt dat elke job-klasse op beide systemen kan draaien, gestuurd door een runtime-flag, zodat je een paar jobs tegelijk kunt migreren en bij problemen direct kunt terugrollen.
Maak een baseklasse die zichzelf routeert:
class ApplicationJob < ActiveJob::Base
before_enqueue :route_to_backend
private
def route_to_backend
backend = JobRouting.backend_for(self.class.name)
self.queue_adapter = case backend
when :solid_queue then :solid_queue
when :sidekiq then :sidekiq
end
end
end
En een routing-module die uit een feature-flag-store leest — Flipper, een databasetabel of omgevingsvariabelen. We behandelden feature-flag-patronen hier in detail.
module JobRouting
SOLID_QUEUE_JOBS = Set.new
def self.backend_for(job_class_name)
return :solid_queue if Flipper.enabled?(:solid_queue_global)
return :solid_queue if Flipper.enabled?("solid_queue_#{job_class_name.underscore}".to_sym)
:sidekiq
end
end
Configureer beide adapters in config/application.rb. Active Job laat je een standaard-adapter zetten, maar queue_adapter= op een individuele job overschrijft die:
config.active_job.queue_adapter = :sidekiq # standaard tijdens migratie
Nu kun je één job-klasse omzetten naar Solid Queue:
Flipper.enable :solid_queue_send_invoice_email_job
Monitor een uur. Als het misgaat, zet je hem uit. De job gaat zonder codewijziging terug naar Sidekiq.
Terugkerende en geplande jobs migreren
Dit is het onderdeel dat de meeste migratiegidsen oppervlakkig behandelen en de meeste teams onderschatten.
Sidekiq-cron bewaart zijn schema in Redis. Solid Queue gebruikt een YAML-configbestand. Je kunt een terugkerende job niet dual-runnen — precies één scheduler moet hem bezitten, anders krijg je dubbele uitvoeringen.
Inventariseer je sidekiq-cron-jobs:
Sidekiq::Cron::Job.all.each do |job|
puts [job.name, job.cron, job.klass].join(" | ")
end
Vertaal elke naar config/recurring.yml:
production:
expire_trial_accounts:
class: ExpireTrialAccountsJob
queue: maintenance
schedule: every day at 3am UTC
reconcile_stripe_charges:
class: ReconcileStripeChargesJob
queue: critical
schedule: every hour at minute 17
Plan een cutover-venster. In één deploy: verwijder de job uit sidekiq-cron, voeg hem toe aan recurring.yml, zorg dat de Solid Queue-workers draaien. Er is een klein venster waarin geen scheduler de job bezit — kies een moment waarop één gemiste run onschadelijk is. Alles waarbij een gemiste run wél belangrijk is (facturatie, compliance) migreer je tijdens een rustig uur met een mens die meekijkt.
Geplande jobs onderweg (PaymentJob.set(wait: 24.hours).perform_later(payment)) zijn anders. Die leven in Sidekiq’s ScheduledSet en vuren wanneer hun perform_at aanbreekt. Laat Sidekiq draaien tot de ScheduledSet leeg is. Voor een normale app duurt dat één tot twee weken.
Solid Queue-workers in productie draaien
Sidekiq draait als één proces per machine met een threadpool. Solid Queue draait als procesbeheerder met meerdere workertypes: workers (doen jobs), dispatchers (pakken geplande jobs op) en de scheduler (terugkerende jobs). Je configureert ze in config/queue.yml:
production:
dispatchers:
- polling_interval: 1
batch_size: 500
workers:
- queues: [critical, default]
threads: 5
processes: 2
polling_interval: 0.1
- queues: [low, maintenance]
threads: 3
processes: 1
polling_interval: 1
scheduler:
recurring_tasks_path: config/recurring.yml
Polling-interval is de knop die mensen verkeerd zetten. Op de critical-queue wil je 0.1s — workers worden elke honderd milliseconden wakker om werk op te pikken. Op een lage-prioriteitsqueue is 1s of meer prima en verlaagt het de databaselast.
Als je deployt met Kamal — en dat zou je moeten doen, zie mijn Kamal 2 productiegids — draai Solid Queue dan als een aparte accessory of role:
servers:
web:
hosts:
- 10.0.0.10
jobs:
hosts:
- 10.0.0.20
cmd: bin/jobs start
options:
memory: 2g
bin/jobs is de supervisor die met solid_queue:install werd meegeleverd. Hij doet graceful shutdown op SIGTERM — jobs onderweg krijgen tot SolidQueue.shutdown_timeout om af te ronden voordat de worker stopt. Zet die hoger dan je langste job. Standaard houd ik twee minuten aan.
Rekenwerk voor de connection pool
Dit is de stille killer van Solid Queue-migraties. Elke worker-thread houdt een Postgres-verbinding vast. Met twee workerprocessen van vijf threads elk heb je per machine al tien verbindingen alleen voor Solid Queue. Tel daar dispatcher, scheduler en je web-Pumas bij op, en je kunt max_connections snel opblazen.
De rekensom:
totale_verbindingen = (web_processen * web_threads)
+ (worker_processen * (worker_threads + 1))
+ dispatcher_verbindingen
+ scheduler_verbindingen
+ speling voor admin/migraties
De meeste teams hebben PgBouncer in transaction-pooling mode voor Postgres nodig zodra ze overschakelen op Solid Queue. Als je al aan de rand van je connection pool zit met Sidekiq, duwt de migratie je eroverheen. Plan dit voordat je je eerste job overzet.
Observability en de Web UI
Sidekiq Web is uitstekend. Mission Control Jobs — de officiële Solid Queue-UI — is goed maar nieuwer. Mount hem:
# config/routes.rb
authenticate :user, ->(u) { u.admin? } do
mount MissionControl::Jobs::Engine, at: "/jobs"
end
Hij geeft je live job-counts, gefaalde jobs met stack traces, retries en een knop om handmatig opnieuw te enqueuen. Voor productie-observability wil je meer. Haak in op de notificaties van Active Job:
ActiveSupport::Notifications.subscribe "perform.active_job" do |*, payload|
StatsD.increment(
"jobs.processed",
tags: ["job:#{payload[:job].class.name}", "queue:#{payload[:job].queue_name}"]
)
end
ActiveSupport::Notifications.subscribe "enqueue.active_job" do |*, payload|
StatsD.increment("jobs.enqueued", tags: ["job:#{payload[:job].class.name}"])
end
Zet Postgres-queries op je dashboard voor queue-diepte en de oudste niet-gestarte job:
SELECT queue_name, COUNT(*) AS depth,
EXTRACT(EPOCH FROM (now() - MIN(created_at))) AS oldest_seconds
FROM solid_queue_ready_executions
GROUP BY queue_name;
Als oldest_seconds op critical ooit je SLO overschrijdt (de mijne is meestal 30), piep je iemand. Dit is hetzelfde mentale model als Sidekiq’s latency-metric, alleen uitgedrukt in SQL.
De cutover
Na twee weken dual-running staat elke job-klasse op Solid Queue en heeft Sidekiq niets meer te doen. Loop de verificatiechecklist door:
# Sidekiq-queues zijn leeg
Sidekiq::Queue.all.map { |q| [q.name, q.size] }.to_h
# => allemaal nullen
# Sidekiq scheduled set is leeg
Sidekiq::ScheduledSet.new.size == 0
# Sidekiq retry set is leeg (of je hebt besloten ze los te laten)
Sidekiq::RetrySet.new.size == 0
# Solid Queue verwerkt
SolidQueue::Job.where("created_at > ?", 5.minutes.ago).count > 0
Deploy daarna de definitieve opruiming: haal sidekiq uit de Gemfile, verwijder config/sidekiq.yml, haal de Sidekiq Web-mount weg, zet config.active_job.queue_adapter = :solid_queue, verwijder de JobRouting-module en haal de Flipper-flags weg. Eén kleine PR, makkelijk te reverten.
Zet de Redis-instance als laatste uit. Wacht een week. Maak een laatste RDB-snapshot voordat je hem termineert. Misschien heb je Redis nooit meer nodig — of misschien ontdek je hem opnieuw voor caching (Solid Cache is hetzelfde idee, maar dan voor caching).
Het rollback-plan
Schrijf dit voordat je begint. Het dual-running-patroon maakt rollback bijna gratis: zet elke Flipper-flag uit en het systeem staat binnen dertig seconden terug op Sidekiq. Het gevarenzone is nadat je Sidekiq uit de Gemfile hebt gehaald en JobRouting hebt verwijderd. Tot dat moment ben je altijd één toggle van veiligheid verwijderd.
Als een job-klasse data corrupt maakt op specifiek Solid Queue — meestal door een aanname over transactionele enqueue die op Sidekiq niet bestond — zet je die ene klasse terug, onderzoekt, fixt, deployt opnieuw en zet hem weer aan. Geen paniek-rollback van de hele migratie om één job-klasse.
FAQ
Hoe lang duurt een typische Sidekiq naar Solid Queue migratie?
Voor een middelgrote Rails-app met twintig tot vijftig job-klassen en gematigd volume reken op vier tot acht weken end-to-end: één week audit en setup, twee tot vier weken geleidelijke cutover met dual-running, één week monitoren terwijl Solid Queue alles doet en een laatste opruimdeploy. Sla de audit niet over.
Kan Solid Queue dezelfde doorvoer aan als Sidekiq?
Voor de meeste apps ja — alles onder tienduizend jobs per minuut draait comfortabel op een goed afgestelde Postgres. Boven vijftigduizend jobs per minuut op één queue ga je de polling-overhead voelen en moet je queues over workerpools sharden of op Sidekiq blijven. Benchmark met je echte workload voordat je het besluit neemt.
Wat gebeurt er met Sidekiq Pro-batches bij een Solid Queue-migratie?
Er is geen directe tegenhanger. Je hebt drie opties: Sidekiq Pro behouden voor batch-workflows en beide systemen onbeperkt blijven draaien, batches herschrijven als parent-child-jobrecords in je eigen database met completion-tracking, of de fan-out/fan-in als state machine modelleren. Ik adviseer meestal de tweede — een JobBatch-tabel met pending_count- en completed_count-kolommen is recht-toe-recht-aan en geeft alsnog betere observability dan Sidekiq-batches.
Heb ik een aparte database nodig voor Solid Queue?
Sterk aanbevolen voor elke productie-app die meer dan een paar honderdduizend jobs per dag verwerkt. De jobtabellen hebben heel andere toegangspatronen dan je applicatietabellen — hoge insert- en delete-rates, veel index-churn — en je wilt niet dat autovacuum daar je gebruikersgerichte queries beïnvloedt. Een aparte database laat je ook fsync en replicatie los tunen als je aan de queue-kant een beetje duurzaamheid voor performance wilt inruilen.
Een Sidekiq naar Solid Queue migratie aan het plannen en wil je er een tweede paar ogen op? TTB Software helpt Rails-teams achtergrondjob-migraties plannen en uitvoeren zonder jobs te verliezen in productie. We doen dit al negentien jaar.
About the Author
Roger Heykoop is een senior Ruby on Rails ontwikkelaar met 19+ jaar Rails ervaring en 35+ jaar ervaring in softwareontwikkeling. Hij is gespecialiseerd in Rails modernisering, performance optimalisatie, en AI-ondersteunde ontwikkeling.
Get in TouchRelated Articles
Need Expert Rails Development?
Let's discuss how we can help you build or modernize your Rails application with 19+ years of expertise
Schedule a Free Consultation