SQS Visibility Timeout verstehen: Warum Nachrichten doppelt verarbeitet werden

Eine Nachricht landet bei zwei verschiedenen Consumern gleichzeitig — das ist kein Bug im Anwendungscode, sondern meistens ein falsch kalibriertes Visibility Timeout in Amazon SQS. Wer diesen Mechanismus nicht genau versteht, baut sich eine Race Condition direkt in die Verarbeitungspipeline ein.

TL;DR: SQS Visibility Timeout auf einen Blick

AspektDetail
Was es istZeitfenster, in dem eine abgerufene Nachricht für andere Consumer unsichtbar ist
Standardwert30 Sekunden
Konfigurierbarer Bereich0 Sekunden bis 12 Stunden
Typische Ursache für DoppelverarbeitungVerarbeitungszeit überschreitet das Visibility Timeout
LösungTimeout an reale Verarbeitungsdauer anpassen oder ChangeMessageVisibility nutzen
Löschen der NachrichtMuss explizit per DeleteMessage erfolgen — passiert nicht automatisch

Wie SQS Visibility Timeout funktioniert

SQS ist kein klassischer Message Broker mit exklusivem Lock. Wenn ein Consumer eine Nachricht per ReceiveMessage abruft, wird sie nicht aus der Queue entfernt — sie wird nur temporär versteckt. Dieses Versteck-Fenster ist das Visibility Timeout. Läuft es ab, bevor der Consumer die Nachricht explizit löscht, taucht sie wieder auf und ist für jeden anderen Consumer sichtbar.

Das ist kein Fehler im Design — es ist eine bewusste Entscheidung für Resilienz. Wenn ein Consumer crasht, soll die Nachricht nicht verloren gehen. Aber genau dieser Mechanismus produziert Doppelverarbeitung, sobald die Verarbeitungszeit das Timeout überschreitet.

sequenceDiagram participant CA as Consumer A participant SQS as SQS Queue participant CB as Consumer B CA->>SQS: ReceiveMessage SQS-->>CA: Nachricht + Receipt Handle Note over SQS: Visibility Timeout startet Note over SQS: Nachricht unsichtbar fuer andere Note over CA: Verarbeitung dauert laenger als Timeout Note over SQS: Timeout abgelaufen Note over SQS: Nachricht wieder sichtbar CB->>SQS: ReceiveMessage SQS-->>CB: Dieselbe Nachricht Note over CA,CB: Beide verarbeiten dieselbe Nachricht
  1. ReceiveMessage: Consumer A ruft die Nachricht ab. Das Visibility Timeout startet sofort.
  2. Unsichtbar: Während des Timeouts ist die Nachricht für alle anderen Consumer verborgen.
  3. Timeout abgelaufen: Wenn Consumer A nicht rechtzeitig löscht, wird die Nachricht wieder sichtbar.
  4. Doppelverarbeitung: Consumer B ruft dieselbe Nachricht ab — parallele Verarbeitung beginnt.
  5. DeleteMessage: Nur ein expliziter Delete-Aufruf entfernt die Nachricht dauerhaft.

Warum das Visibility Timeout so oft falsch konfiguriert ist

Der Standardwert von 30 Sekunden klingt großzügig — bis die Verarbeitungslogik eine externe API aufruft, eine Datenbankabfrage ausführt oder ein großes S3-Objekt verarbeitet. In Produktionssystemen mit variablen Latenzen reichen 30 Sekunden oft nicht aus.

Das tückische daran: Der Fehler zeigt sich nicht sofort. Unter normaler Last verarbeitet Consumer A schnell genug. Erst bei erhöhter Last, langsamen Downstream-Services oder GC-Pausen überschreitet die Verarbeitungszeit das Timeout — und plötzlich verarbeiten zwei Consumer dieselbe Nachricht.

Das Visibility Timeout ist wie ein Parkticket: Solange du zurück bist, bevor es abläuft, passiert nichts. Aber wenn du zu spät kommst, steht jemand anderes auf deinem Parkplatz.

SQS Visibility Timeout konfigurieren und diagnostizieren

Aktuellen Timeout-Wert einer Queue prüfen

Bevor etwas geändert wird, den Ist-Zustand ermitteln. Das Attribut heißt VisibilityTimeout.

aws sqs get-queue-attributes \
  --queue-url https://sqs.us-east-1.amazonaws.com/123456789012/meine-queue \
  --attribute-names VisibilityTimeout

Visibility Timeout auf Queue-Ebene anpassen

Den Wert auf einen realistischen Puffer über der maximalen erwarteten Verarbeitungszeit setzen — nicht nur den Durchschnitt, sondern das 99. Perzentil der Verarbeitungsdauer als Orientierung nehmen.

aws sqs set-queue-attributes \
  --queue-url https://sqs.us-east-1.amazonaws.com/123456789012/meine-queue \
  --attributes VisibilityTimeout=300

Visibility Timeout pro Nachricht verlängern (ChangeMessageVisibility)

Wenn die Verarbeitungsdauer nicht vorhersehbar ist — etwa bei abhängigen externen Calls — kann der Consumer das Timeout aktiv verlängern, während er noch arbeitet. Das ist die robustere Lösung für variable Workloads.

aws sqs change-message-visibility \
  --queue-url https://sqs.us-east-1.amazonaws.com/123456789012/meine-queue \
  --receipt-handle "AQEB...receiptHandle..." \
  --visibility-timeout 120

Der receipt-handle wird bei jedem ReceiveMessage-Aufruf zurückgegeben und ist pro Empfang eindeutig. Wichtig: Dieser Aufruf muss erfolgen, bevor das aktuelle Timeout abläuft — nicht danach.

Nachricht nach erfolgreicher Verarbeitung löschen

Das Löschen passiert nicht automatisch. Wer das vergisst, sieht jede Nachricht nach Ablauf des Timeouts erneut — auch wenn die Verarbeitung erfolgreich war.

aws sqs delete-message \
  --queue-url https://sqs.us-east-1.amazonaws.com/123456789012/meine-queue \
  --receipt-handle "AQEB...receiptHandle..."

Diagnosepfad: Doppelverarbeitung systematisch eingrenzen

Das Symptom ist klar — zwei Consumer verarbeiten dieselbe Nachricht. Die Ursache ist meistens eine von drei: Timeout zu kurz, Delete fehlt, oder der Consumer gibt den Receipt Handle nicht korrekt weiter.

graph TD A["Doppelverarbeitung beobachtet"] --> B["VisibilityTimeout pruefen"] B --> C{"Timeout < max. Verarbeitungszeit?"} C -- Ja --> D["Timeout erhoehen oder
ChangeMessageVisibility nutzen"] C -- Nein --> E["DeleteMessage-Aufruf pruefen"] E --> F{"Delete wird aufgerufen?"} F -- Nein --> G["Delete nach Verarbeitung implementieren"] F -- Ja --> H["Receipt Handle korrekt?"] H -- Nein --> I["Handle aus aktuellem ReceiveMessage verwenden"] H -- Ja --> J["CloudWatch Metriken pruefen"] D --> K["Problem geloest"] G --> K I --> K
  1. ApproximateNumberOfMessagesNotVisible prüfen: Dieser CloudWatch-Metrikwert zeigt, wie viele Nachrichten aktuell 'in flight' sind. Ein dauerhaft hoher Wert deutet auf Nachrichten hin, die nicht rechtzeitig gelöscht werden.
  2. Verarbeitungszeit messen: Eigene Anwendungsmetriken oder X-Ray-Traces zeigen, ob die Verarbeitungsdauer das konfigurierte Timeout überschreitet.
  3. DeleteMessage-Aufrufe verifizieren: Sicherstellen, dass der Delete-Aufruf im Erfolgsfall tatsächlich ausgeführt wird — nicht nur im Happy Path, sondern auch nach Retries.

CloudWatch-Metrik abfragen

aws cloudwatch get-metric-statistics \
  --namespace AWS/SQS \
  --metric-name ApproximateNumberOfMessagesNotVisible \
  --dimensions Name=QueueName,Value=meine-queue \
  --start-time 2024-01-15T10:00:00Z \
  --end-time 2024-01-15T11:00:00Z \
  --period 300 \
  --statistics Average

Erfahrung aus der Praxis: Die falsche Diagnose

Ein Team beobachtete Doppelverarbeitung nur unter Last — nie in der Staging-Umgebung. Die erste Vermutung war ein Bug im Consumer-Code: irgendwo wird ReceiveMessage zweimal aufgerufen. Stunden wurden damit verbracht, den Anwendungscode zu durchsuchen.

Die eigentliche Ursache: Das Visibility Timeout war auf 30 Sekunden gesetzt. Die Verarbeitung dauerte unter normaler Last 8-10 Sekunden — kein Problem. Unter Last, wenn ein nachgelagerter Datenbankcluster unter Druck geriet, stieg die Verarbeitungszeit auf 35-45 Sekunden. Das Timeout lief ab, die Nachricht wurde wieder sichtbar, ein zweiter Consumer griff zu.

Der entscheidende Hinweis kam nicht aus dem Anwendungslog, sondern aus der CloudWatch-Metrik ApproximateNumberOfMessagesNotVisible kombiniert mit der Datenbanklatenz. Sobald die DB-Latenz stieg, stieg auch die Doppelverarbeitungsrate — eine direkte Korrelation, die den Timeout-Zusammenhang sofort offenbarte.

Fix: Timeout auf 120 Sekunden erhöht, zusätzlich ChangeMessageVisibility als Heartbeat alle 60 Sekunden implementiert. Doppelverarbeitung seitdem null.

IAM-Berechtigungen für Visibility-Timeout-Operationen

Consumer benötigen explizite Berechtigungen für alle drei relevanten Aktionen. Fehlende ChangeMessageVisibility-Berechtigung ist ein häufiger Grund, warum der Heartbeat-Ansatz in Produktion lautlos scheitert.

🔽 IAM-Policy für SQS Consumer (klicken zum Aufklappen)
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "sqs:ReceiveMessage",
        "sqs:DeleteMessage",
        "sqs:ChangeMessageVisibility",
        "sqs:GetQueueAttributes"
      ],
      "Resource": "arn:aws:sqs:us-east-1:123456789012:meine-queue"
    }
  ]
}

Dead Letter Queue als Sicherheitsnetz

Selbst mit korrektem Timeout-Management können Nachrichten in eine Endlosschleife geraten, wenn die Verarbeitung dauerhaft fehlschlägt. Eine Dead Letter Queue (DLQ) fängt Nachrichten auf, die eine konfigurierte Anzahl von Empfangsversuchen (maxReceiveCount) überschreiten.

aws sqs set-queue-attributes \
  --queue-url https://sqs.us-east-1.amazonaws.com/123456789012/meine-queue \
  --attributes '{
    "RedrivePolicy": "{\"deadLetterTargetArn\":\"arn:aws:sqs:us-east-1:123456789012:meine-queue-dlq\",\"maxReceiveCount\":\"5\"}"
  }'

Der maxReceiveCount-Wert sollte höher sein als die erwartete Anzahl legitimer Retries — sonst landen valide Nachrichten in der DLQ, bevor sie erfolgreich verarbeitet werden konnten.

SQS Visibility Timeout: Zusammenfassung und nächste Schritte

Doppelverarbeitung durch SQS ist fast immer auf ein zu kurz konfiguriertes Visibility Timeout zurückzuführen. Die Lösung ist nicht kompliziert, aber sie erfordert ein genaues Verständnis der eigenen Verarbeitungsdauer — inklusive Ausreißer unter Last.

  • Visibility Timeout immer auf Basis des 99. Perzentils der Verarbeitungszeit konfigurieren, nicht des Durchschnitts
  • Für variable Workloads ChangeMessageVisibility als Heartbeat implementieren
  • DeleteMessage explizit nach erfolgreicher Verarbeitung aufrufen — nie davon ausgehen, dass es automatisch passiert
  • DLQ einrichten, um dauerhaft fehlschlagende Nachrichten zu isolieren
  • CloudWatch-Metrik ApproximateNumberOfMessagesNotVisible monitoren

Weiterführende Dokumentation: AWS SQS Visibility Timeout — offiziell

Glossar

BegriffBedeutung
Visibility TimeoutZeitfenster nach dem Abruf einer Nachricht, in dem sie für andere Consumer unsichtbar ist
Receipt HandleEindeutiger Bezeichner für einen spezifischen Empfangsvorgang einer Nachricht — wird für Delete und ChangeMessageVisibility benötigt
In-Flight MessageNachricht, die abgerufen wurde, aber noch nicht gelöscht ist — zählt gegen das In-Flight-Limit der Queue
Dead Letter Queue (DLQ)Separate Queue, in die Nachrichten verschoben werden, die zu oft erfolglos verarbeitet wurden
ChangeMessageVisibilityAPI-Aufruf zum Verlängern des Visibility Timeouts einer bereits abgerufenen Nachricht

Kommentare

Beliebte Posts aus diesem Blog

EC2 ohne Internetzugang im eigenen VPC – Internet Gateway und Route Table korrekt einrichten

EC2 SSH Verbindungs-Timeout: Security Group Inbound-Regeln richtig konfigurieren