9. Oktober 2026
Das Problem ist nicht sichtbar im Code
Der Wechsel von direkter Sidekiq-Nutzung zu ActiveJob ist oft bewusst klein gehalten: Ein Job erbt künftig von ApplicationJob, die vorhandenen sidekiq_options bleiben zunächst stehen. Das wirkt plausibel, denn Sidekiq verarbeitet den Job weiterhin.
Genau darin liegt die Falle. Unter dem Sidekiq-Adapter von ActiveJob haben nicht alle Optionen dieselbe Wirkung wie bei einem direkten Sidekiq-Worker. Insbesondere kann sidekiq_options queue: stehen bleiben und dennoch ignoriert werden. Der Job läuft dann auf default.
Im beschriebenen Fall wären dadurch alle fünf Wartungs-Jobs auf der default-Queue gelaufen. Die Test-Suite blieb grün, weil sie zwar prüfte, dass Jobs eingeplant wurden, nicht aber auf welcher Queue sie tatsächlich ankamen.
Vorher: Direkter Sidekiq-Worker
Bei einem klassischen Worker ist die Queue-Konfiguration direkt Teil der Sidekiq-Deklaration:
class CleanupWorker
include Sidekiq::Worker
sidekiq_options queue: :maintenance, retry: 5
def perform(account_id)
# Aufräumlogik
end
endHier beschreibt sidekiq_options queue: :maintenance die Ziel-Queue des Workers. Beim Umbau zu Sidekiq ActiveJob ändert sich aber die Zuständigkeit: ActiveJob definiert Queue und Serialisierung, der Sidekiq-Adapter verpackt den Auftrag anschließend für Sidekiq.
Nachher: Queue mit Rails queue_as festlegen
Für ActiveJob gehört die Queue daher in die ActiveJob-API:
class CleanupJob < ApplicationJob
queue_as :maintenance
sidekiq_options retry: 5
def perform(account_id)
# Aufräumlogik
end
endDas ist keine Empfehlung gegen ActiveJob. Im Gegenteil: Wer Sidekiq ActiveJob verwendet, sollte die jeweilige Verantwortung klar zuordnen. queue_as ist die passende Deklaration für die Queue. Sidekiq-spezifische Optionen müssen dagegen einzeln darauf geprüft werden, ob sie über den Adapter weiterhin greifen.
Tückisch ist, dass retry: weiter wirken kann. Eine Mischung aus funktionierenden und wirkungslosen sidekiq_options erzeugt ein falsches Sicherheitsgefühl: Der Job wird verarbeitet, Wiederholungen funktionieren, aber das Routing ist falsch.
Tests müssen die gepushte Payload prüfen
Ein Test wie expect { CleanupJob.perform_later(42) }.to have_enqueued_job reicht nicht aus. Er beweist nur, dass ActiveJob einen Auftrag angenommen hat. Er beweist weder den Queue-Namen noch die Form der Argumente nach dem Sidekiq-Adapter.
Ich würde einen Test ergänzen, der die tatsächlich gepushte Sidekiq-Payload auswertet. Das genaue Hilfswerkzeug hängt vom Test-Setup ab; relevant sind aber immer mindestens Queue und Argumente.
CleanupJob.perform_later(42)
payload = pushed_sidekiq_payloads.last
expect(payload["queue"]).to eq("maintenance")
expect(payload["args"].first["arguments"]).to eq([42])Das Beispiel zeigt bewusst die Ebene, die zählt: nicht die Ruby-Klasse und nicht nur die ActiveJob-Test-Queue, sondern die Nachricht nach der Übersetzung für Sidekiq. Je nach Rails- und Test-Setup kann die Hilfsmethode anders heißen. Auch die Payload enthält zusätzliche Metadaten. Entscheidend ist die Prüfung des tatsächlichen Queue-Namens und der serialisierten Argumente.
retries_exhausted erhält andere Argumente
Besondere Aufmerksamkeit verdient sidekiq_retries_exhausted. Bei direkten Sidekiq-Workern enthält msg['args'] die ursprünglichen Argumente des Workers. Bei Sidekiq ActiveJob enthält msg['args'] stattdessen die ActiveJob-Payload als Hash.
Code, der etwa eine ID direkt aus msg['args'] liest, muss deshalb angepasst werden. Die ursprünglichen Argumente liegen innerhalb der ActiveJob-Struktur:
sidekiq_retries_exhausted do |msg, exception|
job_payload = msg["args"].first
account_id = job_payload["arguments"].first
# Fehlerbehandlung mit account_id
endAuch das sollte ein Test abdecken. Sonst fällt die Abweichung erst auf, wenn ein Retry endgültig erschöpft ist.
Sidekiq Capsules: Queue-Kapazität bewusst planen
Seit Sidekiq 7 können Sidekiq Capsules einer Queue eigene Threads geben. Das ist nützlich, wenn etwa Wartungs-Jobs getrennt von anderen Hintergrundaufgaben verarbeitet werden sollen. Das Thread-Limit gilt dabei pro Prozess und wird beim Boot gelesen.
Die Konsequenz ist praktisch: Eine Queue-Zuordnung ist nicht nur ein Label. Sie kann bestimmen, welche Verarbeitungskapazität ein Job erhält. Wenn ein Job wegen einer ignorierten sidekiq_options queue auf default landet, greifen auch die Überlegungen zu Sidekiq Capsules und getrennten Threads nicht wie geplant.
Mein Rat ist daher schlicht: Nach einer Migration nicht nur den Code vergleichen. Prüft die Redis-nahe Payload, den Queue-Namen, die Argumentstruktur und die Retry-Callbacks. Schaut nach, auf welcher Queue eure Jobs wirklich laufen.