Zum Inhalt springen
Projekt besprechen

15. Oktober 2026

Dein Sidekiq-Job ist nicht idempotent. Beim Retry merkst du es.

SidekiqRailsRubyEngineering

Sidekiq liefert mindestens einmal, nicht genau einmal

Ein Sidekiq-Job ist kein einmaliger Funktionsaufruf. Sidekiq arbeitet mit at-least-once-Ausführung: Nach einem Fehler kann ein Sidekiq Retry denselben Job erneut ausführen. Auch nach einem Prozessabsturz oder Neustart kann ein Job wieder anlaufen.

Das ist kein Fehler von Sidekiq, sondern eine Eigenschaft verteilter Verarbeitung. Entscheidend ist die Konsequenz im eigenen Code: Ein Job muss damit umgehen können, dass er mehrfach startet.

Ein typisches Risiko sieht so aus: Der Job erzeugt zuerst einen Datensatz oder bucht einen Betrag, versendet danach eine Mail und scheitert anschließend. Beim Retry beginnt er wieder am Anfang. Alles, was vor dem Fehler bereits Wirkung hatte, kann ein zweites Mal passieren.

Was Idempotenz konkret bedeutet

Ein Sidekiq idempotent gebauter Job hat bei mehrfacher Ausführung dieselbe Wirkung wie bei einmaliger Ausführung. Gleiche Eingabe, mehrfach ausgeführt, gleiche Wirkung.

Das heißt nicht, dass jeder Codeabschnitt exakt einmal läuft. Eine Datenbankabfrage darf beliebig oft stattfinden. Relevant sind Seiteneffekte:

  • einen Datensatz anlegen
  • einen Status ändern
  • eine Mail senden
  • eine Zahlung auslösen
  • einen Webhook oder API-Aufruf verschicken

Angenommen, ein Job soll für eine Bestellung eine Rechnung erzeugen. Ein einfacher Ansatz wäre, bei jedem Lauf eine Rechnung anzulegen. Beim Retry entstehen dann mehrere Rechnungen für dieselbe Bestellung.

Die fachliche Regel lautet aber vermutlich: Pro Bestellung darf es nur eine Rechnung geben. Diese Regel gehört nicht nur in Ruby-Code, sondern in die Datenbank.

Eindeutige Constraints sind die letzte Instanz

Eine Vorabprüfung allein ist nicht ausreichend. Zwei Worker können nahezu gleichzeitig prüfen, ob eine Rechnung existiert, und beide anschließend versuchen, eine anzulegen. Ein eindeutiger Datenbank-Constraint verhindert, dass die Regel dabei verletzt wird.

class CreateInvoiceJob
  include Sidekiq::Job
 
  def perform(order_id)
    Invoice.create_or_find_by!(order_id: order_id)
  end
end

Dazu braucht die Tabelle einen eindeutigen Index auf order_id:

add_index :invoices, :order_id, unique: true

create_or_find_by! bildet zusammen mit dem Constraint ein brauchbares Muster für diesen Fall. Der Code versucht die Anlage, die Datenbank erzwingt die Eindeutigkeit. Läuft der Job erneut, wird dieselbe Rechnung verwendet statt eine zweite zu erzeugen.

Bei komplexeren Vorgängen kann ein Idempotenz-Schlüssel passender sein. Das ist ein stabiler Wert, der eine fachliche Operation identifiziert, etwa die Kombination aus Bestell-ID und Vorgangstyp. Dieser Schlüssel wird gespeichert und ebenfalls eindeutig abgesichert.

Status vor dem Seiteneffekt prüfen

Ein weiteres Muster ist ein expliziter Status. Der Job prüft vor der Wirkung, ob sie bereits abgeschlossen wurde.

class GenerateReportJob
  include Sidekiq::Job
 
  def perform(report_id)
    report = Report.find(report_id)
    return if report.generated?
 
    report.generate!
    report.update!(status: "generated")
  end
end

Das Beispiel zeigt die Absicht, ist aber nicht automatisch vollständig robust. Scheitert der Prozess nach generate! und vor dem Status-Update, kennt der Retry die bereits erfolgte Wirkung nicht. Die Statusänderung, die eigentliche Wirkung und die Fehlerbehandlung müssen deshalb als zusammenhängender Ablauf entworfen werden.

Für Datenbankänderungen helfen Transaktionen, sofern alle beteiligten Änderungen in derselben Datenbank stattfinden. Für externe Systeme gilt diese Garantie jedoch nicht.

Externe Seiteneffekte brauchen einen eigenen Schutz

Mailversand, Zahlungsanbieter und fremde APIs liegen außerhalb der eigenen Datenbanktransaktion. Zwischen API-Aufruf und lokalem Status-Update kann ein Fehler auftreten. Dann ist unklar, ob der externe Seiteneffekt bereits eingetreten ist.

Deshalb brauchen externe Seiteneffekte einen eigenen Schlüssel oder einen idempotenten API-Aufruf. Falls ein Anbieter Idempotenz-Schlüssel unterstützt, sollte derselbe fachliche Schlüssel bei jedem Retry erneut verwendet werden. Der Anbieter kann damit erkennen, dass es um dieselbe Operation geht.

Bei einer Mail kann eine eigene Versandtabelle helfen: Sie speichert den fachlichen Schlüssel und den Versandstatus. Das verhindert doppelte Verarbeitung im eigenen System, löst aber nicht automatisch die Lücke zwischen tatsächlichem Versand und Status-Update. Diese Grenze muss bewusst behandelt werden.

Die praktische Frage für jeden Hintergrundjob lautet daher nicht, ob ein Retry wahrscheinlich ist. Er ist vorgesehen. Entscheidend ist, welche Wirkung beim zweiten Lauf eintreten darf und welche nicht.

Prüft einen eurer Jobs: Was passiert, wenn er zweimal läuft?