---
title: 'Ausfallsichere Integrationen: Timeouts, Retry, Circuit Breaker und Idempotenz | DevSense'
description: 'Wie Sie Zahlungsanbieter und Provider-APIs ohne doppelte Abbuchungen und kaskadierende Ausfälle anbinden: Timeouts, Retries mit Backoff, Circuit Breaker, Idempotenzschlüssel und Abgleich in Laravel.'
faq:
    - { question: 'Warum ist ein Timeout der gefährlichste Ausgang eines externen API-Aufrufs?', answer: 'Bei einem Fehler wissen Sie, dass die Operation nicht ausgeführt wurde, bei einem Erfolg, dass sie ausgeführt wurde. Bei einem Timeout wissen Sie gar nichts: Der Anbieter hat das Geld möglicherweise abgebucht und nur nicht mehr rechtzeitig geantwortet. Ein blinder Retry führt in diesem Fall zu einer doppelten Abbuchung. Deshalb werden Geldoperationen nur mit Idempotenzschlüssel oder nach einer Statusabfrage beim Anbieter wiederholt.' }
    - { question: 'Welche Fehler lassen sich gefahrlos wiederholen?', answer: 'Netzwerk- und Verbindungsfehler sowie Antworten mit 429 und 5xx – sofern die Operation idempotent ist oder durch einen Idempotenzschlüssel geschützt wird. 4xx-Antworten (außer 408 und 429) zu wiederholen ist sinnlos: Der Request ist fehlerhaft und wird erneut abgelehnt. Retries erfolgen mit exponentieller Verzögerung und zufälliger Streuung (Jitter), damit beim Anbieter kein Request-Sturm entsteht.' }
    - { question: 'Worin unterscheidet sich ein Circuit Breaker von einem Retry?', answer: 'Ein Retry versucht, eine kurzzeitige Störung eines einzelnen Requests zu überbrücken. Ein Circuit Breaker schützt Ihr System vor einem längeren Ausfall des Anbieters: Nach einer Reihe von Fehlern sendet er keine Requests mehr und liefert sofort einen Fehler oder einen Fallback, ohne Worker mit Warten zu blockieren. Nach einer Pause lässt er einen Probe-Request durch und gibt den Traffic wieder frei, wenn dieser erfolgreich ist.' }
    - { question: 'Wie stellt man sicher, dass Geld nicht zweimal abgebucht wird?', answer: 'Eine „Exactly-once“-Garantie für die Zustellung von Nachrichten gibt es nicht, wohl aber genau einen Effekt. Dazu erhält jede Operation einen eindeutigen Schlüssel, der vor dem Aufruf des Anbieters mit einer UNIQUE-Constraint in der Datenbank gespeichert und im Header Idempotency-Key an den Anbieter übergeben wird. Ein wiederholter Request oder ein wiederholter Webhook mit demselben Schlüssel liefert das bereits gespeicherte Ergebnis statt einer neuen Operation.' }
published: '2026-09-28'
---
# Ausfallsichere Integrationen: Timeouts, Retry, Circuit Breaker und Idempotenz

Der Zahlungsanbieter antwortete plötzlich nach 30 Sekunden statt nach 300 Millisekunden. Eine Minute später war die gesamte Seite down, einschließlich der Seiten, die mit Zahlungen überhaupt nichts zu tun haben: Alle PHP-FPM-Worker hingen im Warten auf eine Antwort. Als der Anbieter wieder lief, zeigte sich das zweite Problem: Ein Teil der Nutzer hatte die Bestellung doppelt bezahlt. Der Code hatte den Request nach dem Timeout wiederholt, obwohl der erste Request in Wirklichkeit durchgegangen war. Keine einzige Codezeile war „falsch“. Der externe Aufruf war einfach so geschrieben, als wäre er ein Aufruf einer lokalen Funktion.

**Verwandte Leitfäden:** [Microservice-Patterns: Saga, CQRS, Circuit Breaker](../microservices/microservice-patterns) · [Verteilte Transaktionen](database-and-distributed-transactions) · [Message Queues im Vergleich](message-queues-compared)

## Inhalt

* [Die drei Ausgänge eines externen Aufrufs](#three-outcomes)
* [Timeouts: ein Budget, kein Standardwert](#timeouts)
* [Retry: was, wann und wie wiederholen](#retry)
* [Idempotenz: ein Effekt statt zwei Abbuchungen](#idempotency)
* [Eingehende Webhooks und konkurrierende Abbuchungen](#webhooks)
* [Circuit Breaker: aufhören, jemanden anzurufen, der nicht abnimmt](#circuit-breaker)
* [Isolation: eigene Queues für jeden Anbieter](#bulkheads)
* [Wo der Ansatz an seine Grenzen stößt](#limitations)
* [Häufige Fehler](#common-mistakes)
* [Checkliste](#checklist)
* [Selbsttest-Quiz](#self-test-quiz)

---

<a id="three-outcomes"></a>
## Die drei Ausgänge eines externen Aufrufs

Der Aufruf einer lokalen Methode hat zwei Ausgänge: Sie liefert ein Ergebnis oder wirft eine Exception. Ein Aufruf über das Netzwerk hat drei:

1. **Erfolg** – der Anbieter hat die Operation ausgeführt, und Sie haben eine Antwort erhalten.
2. **Fehler** – der Anbieter hat geantwortet, dass die Operation nicht ausgeführt wurde (oder die Verbindung kam gar nicht zustande).
3. **Unbekannt** – Timeout, Verbindungsabbruch nach dem Senden des Requests, 502 vom Load Balancer. Die Operation kann ausgeführt worden sein – oder auch nicht.

**Eine robuste Integration ist Code, der den dritten Ausgang explizit behandelt: Er begrenzt die Wartezeit, wiederholt nur sichere Operationen, hört auf, einen nicht funktionierenden Anbieter anzurufen, und macht mithilfe von Idempotenzschlüsseln aus Wiederholungen einen einzigen Effekt.**

Alles Weitere in diesem Artikel sind Wege, „unbekannt“ nicht mit „Fehler“ zu verwechseln.

---

<a id="timeouts"></a>
## Timeouts: ein Budget, kein Standardwert

Der HTTP-Client von Laravel wartet standardmäßig bis zu 30 Sekunden auf eine Antwort. Rechnen wir durch, was das für eine Seite mit 50 PHP-FPM-Workern bedeutet, wenn der Anbieter „hängt“:

* jeder Request auf die Bezahlseite belegt einen Worker für 30 Sekunden;
* bei 2 Requests pro Sekunde sind nach 25 Sekunden alle 50 Worker belegt;
* die übrigen Seiten liefern plötzlich 502, obwohl mit ihnen alles in Ordnung ist.

Ein Timeout ist ein Budget, das Sie dem Anbieter zugestehen – ausgehend von seiner üblichen Latenz und davon, wie viele Worker Sie riskieren können zu blockieren:

```php
// app/Services/Payments/PaymentGatewayClient.php
<?php

declare(strict_types=1);

namespace App\Services\Payments;

use Illuminate\Http\Client\PendingRequest;
use Illuminate\Support\Facades\Http;

final class PaymentGatewayClient
{
    private function http(): PendingRequest
    {
        return Http::baseUrl(config('services.gateway.url'))
            ->withToken(config('services.gateway.token'))
            ->connectTimeout(2)  // TCP/TLS-Verbindung aufbauen
            ->timeout(5)         // die vollständige Antwort erhalten
            ->acceptJson();
    }
}
```

> [!TIP]
> **Nehmen Sie lange Aufrufe aus dem HTTP-Request des Nutzers heraus.** Wenn der Anbieter Sekunden zum Antworten braucht, senden Sie den Request aus einem Queue-Job und zeigen Sie dem Nutzer den Status „Zahlung wird verarbeitet“. Dann blockiert ein langsamer Anbieter die Queue-Worker und nicht die Web-Worker.

---

<a id="retry"></a>
## Retry: was, wann und wie wiederholen

Ein Retry hilft, eine kurzzeitige Störung zu überbrücken: einen Pod-Neustart beim Anbieter, einen Netzwerk-Blip, eine `429`-Antwort. Für einen Retry gelten aber drei Bedingungen.

**1. Nur wiederholen, was sich gefahrlos wiederholen lässt.** `GET`-Requests, Statusabfragen, Operationen mit Idempotenzschlüssel. Eine Abbuchung ohne Schlüssel darf nach einem Timeout niemals wiederholt werden.

**2. Nur wiederholbare Fehler wiederholen.** Verbindungsfehler, `429`, `5xx`. Eine Antwort `422` oder `400` bedeutet, dass der Request fehlerhaft ist, und ein Retry bekommt dieselbe Antwort.

**3. Mit wachsender Pause und zufälliger Streuung wiederholen.** Wenn tausend Clients einen Request exakt nach einer Sekunde wiederholen, trifft den Anbieter im Moment der Wiederherstellung ein synchroner Schlag.

```php
// app/Services/Payments/PaymentGatewayClient.php (Fortsetzung)
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Http\Client\RequestException;
use Throwable;

public function paymentStatus(string $paymentId): array
{
    return $this->http()
        ->retry(
            3,
            // Exponentielle Pause mit Jitter: ~200, ~400, ~800 ms
            fn (int $attempt): int => (int) (100 * 2 ** $attempt + random_int(0, 100)),
            fn (Throwable $e): bool => $e instanceof ConnectionException
                || ($e instanceof RequestException
                    && ($e->response->serverError() || $e->response->status() === 429)),
        )
        ->get("/payments/{$paymentId}")
        ->throw()
        ->json();
}
```

Für Queue-Jobs wird dasselbe über Eigenschaften des Jobs festgelegt:

```php
// app/Jobs/CapturePayment.php
public int $tries = 5;

/** @return list<int> Pausen zwischen den Versuchen in Sekunden */
public function backoff(): array
{
    return [10, 30, 60, 300];
}
```

> [!WARNING]
> **Retries multiplizieren sich.** Wenn der HTTP-Client 3 Versuche macht, der Queue-Job 5 und der aufrufende Service ebenfalls wiederholt, erhält der Anbieter bis zu 15+ Requests für eine einzige Operation. Wiederholen Sie auf genau einer Ebene.

---

<a id="idempotency"></a>
## Idempotenz: ein Effekt statt zwei Abbuchungen

Eine „Exactly-once“-Garantie ist im Netzwerk nicht zu haben: Entweder Sie können eine Nachricht verlieren, oder Sie können sie doppelt zustellen. Das realistische Ziel ist **„At-least-once“-Zustellung plus idempotente Verarbeitung**, was genau einen Effekt ergibt.

Das Schema für eine ausgehende Abbuchung:

1. Den Schlüssel der Operation **vor** dem Aufruf erzeugen und die Operation mit Status `pending` und einer UNIQUE-Constraint auf den Schlüssel in der Datenbank speichern.
2. Den Schlüssel im Header `Idempotency-Key` an den Anbieter übergeben. Anbieter wie Stripe liefern auf einen wiederholten Request mit demselben Schlüssel das Ergebnis des ersten zurück, statt die Operation erneut auszuführen.
3. Das Ergebnis speichern. Bei einem Timeout den Status `unknown` setzen und die Wahrheit über eine Statusabfrage herausfinden, nicht über eine blind wiederholte Abbuchung.

```php
// database/migrations/2026_09_28_100000_create_payments_table.php
Schema::create('payments', function (Blueprint $table) {
    $table->id();
    $table->uuid('idempotency_key')->unique();
    $table->foreignId('order_id')->constrained();
    $table->unsignedBigInteger('amount');         // in kleinsten Währungseinheiten (Kopeken/Cent)
    $table->string('currency', 3);
    $table->string('status', 16)->index();        // pending | succeeded | failed | unknown
    $table->string('provider_payment_id')->nullable()->unique();
    $table->timestamps();
});
```

```php
// app/Services/Payments/ChargeOrder.php
<?php

declare(strict_types=1);

namespace App\Services\Payments;

use App\Models\Order;
use App\Models\Payment;
use Illuminate\Database\UniqueConstraintViolationException;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Support\Str;

final class ChargeOrder
{
    public function __construct(private PaymentGatewayClient $gateway) {}

    public function __invoke(Order $order, string $idempotencyKey): Payment
    {
        try {
            $payment = Payment::create([
                'idempotency_key' => $idempotencyKey,
                'order_id' => $order->id,
                'amount' => $order->total,
                'currency' => $order->currency,
                'status' => 'pending',
            ]);
        } catch (UniqueConstraintViolationException) {
            // Erneuter Klick oder Job-Retry: Die Operation existiert bereits
            return Payment::where('idempotency_key', $idempotencyKey)->firstOrFail();
        }

        try {
            $response = $this->gateway->charge($payment); // übergibt Idempotency-Key
            $payment->update([
                'status' => $response['status'] === 'succeeded' ? 'succeeded' : 'failed',
                'provider_payment_id' => $response['id'],
            ]);
        } catch (ConnectionException) {
            // Ausgang unbekannt: Abbuchung nicht wiederholen, sondern später den Status prüfen
            $payment->update(['status' => 'unknown']);
            ReconcilePayment::dispatch($payment->id)->delay(now()->addMinute());
        }

        return $payment;
    }
}
```

Der Idempotenzschlüssel wird dort erzeugt, wo die Absicht des Nutzers entsteht: zum Beispiel beim Öffnen der Checkout-Seite, von wo er als verstecktes Formularfeld mitgeschickt wird. Dann wird auch ein Doppelklick auf den Button „Bezahlen“ zu einer einzigen Operation.

Der Job `ReconcilePayment` fragt beim Anbieter den Status anhand des Schlüssels oder der `provider_payment_id` ab und setzt die Zahlung auf `succeeded` oder `failed`. Der Abgleich ist keine Krücke, sondern fester Bestandteil jeder Integration, bei der es um Geld geht: Sobald eine Zahlung auf `unknown` steht, kennt nur der Anbieter die Wahrheit.

---

<a id="webhooks"></a>
## Eingehende Webhooks und konkurrierende Abbuchungen

Anbieter stellen Webhooks „at least once“ zu: Dasselbe Event kann zweimal ankommen, gleichzeitig auf zwei Nodes oder früher als Ihre synchrone Antwort. Der Handler muss idempotent sein:

```php
// app/Http/Controllers/Webhooks/GatewayWebhookController.php
public function __invoke(GatewayWebhookRequest $request): Response
{
    $event = $request->validatedEvent(); // einschließlich Signaturprüfung

    DB::transaction(function () use ($event): void {
        // UNIQUE(provider, event_id): Der zweite Webhook fügt nichts ein
        $inserted = DB::table('processed_webhooks')->insertOrIgnore([
            'provider' => 'gateway',
            'event_id' => $event['id'],
            'created_at' => now(),
        ]);

        if ($inserted === 0) {
            return; // bereits verarbeitet
        }

        $payment = Payment::where('provider_payment_id', $event['payment_id'])
            ->lockForUpdate()
            ->firstOrFail();

        $payment->update(['status' => $event['status']]);
    });

    return response()->noContent();
}
```

Für ein internes Guthaben (Wallet, Bonuspunkte) braucht es zusätzlich einen Schutz vor der Race Condition zweier gleichzeitiger Abbuchungen. `SELECT ... FOR UPDATE` serialisiert die Operationen auf einer Zeile:

```php
// app/Services/Wallet/DebitWallet.php
DB::transaction(function () use ($walletId, $amount, $operationId): void {
    $wallet = Wallet::whereKey($walletId)->lockForUpdate()->firstOrFail();

    if ($wallet->balance < $amount) {
        throw new InsufficientFunds();
    }

    // UNIQUE(operation_id) im Buchungsjournal – eine weitere Barriere gegen Wiederholung
    $wallet->entries()->create(['operation_id' => $operationId, 'amount' => -$amount]);
    $wallet->decrement('balance', $amount);
});
```

Mehr zu pessimistischen und optimistischen Sperren finden Sie im Artikel über [verteilte Transaktionen](database-and-distributed-transactions).

---

<a id="circuit-breaker"></a>
## Circuit Breaker: aufhören, jemanden anzurufen, der nicht abnimmt

Retries und Timeouts retten einen einzelnen Request. Ist der Anbieter aber zehn Minuten lang down, verbraucht trotzdem jeder Request das gesamte Timeout-Budget und alle Retries. Ein Circuit Breaker **öffnet den Stromkreis** nach einer Reihe von Fehlern: Requests an den Anbieter enden sofort mit einem Fehler oder einem Fallback, und alle N Sekunden wird ein Probe-Request durchgelassen.

Laravel bringt keinen fertigen Circuit Breaker für den HTTP-Client mit, aber auf Basis atomarer Cache-Operationen (Redis) ist er in ein paar Dutzend Zeilen geschrieben:

```php
// app/Support/CircuitBreaker.php
<?php

declare(strict_types=1);

namespace App\Support;

use Closure;
use Illuminate\Support\Facades\Cache;
use RuntimeException;
use Throwable;

final class CircuitBreaker
{
    public function __construct(
        private string $name,
        private int $failureThreshold = 5,   // Fehler in Folge bis zum Öffnen
        private int $openSeconds = 30,       // Pause vor dem Probe-Request
    ) {}

    /**
     * @template T
     * @param Closure(): T $call
     * @return T
     */
    public function call(Closure $call): mixed
    {
        if (Cache::has($this->key('open'))) {
            // Nach der Pause genau einen Probe-Request durchlassen (half-open)
            if (! Cache::add($this->key('probe'), true, $this->openSeconds)) {
                throw new RuntimeException("Circuit {$this->name} is open");
            }
        }

        try {
            $result = $call();
        } catch (Throwable $e) {
            $failures = Cache::increment($this->key('failures'));
            if ($failures >= $this->failureThreshold) {
                Cache::put($this->key('open'), true, $this->openSeconds * 10);
            }
            throw $e;
        }

        Cache::forget($this->key('failures'));
        Cache::forget($this->key('open'));

        return $result;
    }

    private function key(string $suffix): string
    {
        return "circuit:{$this->name}:{$suffix}";
    }
}
```

```php
// Verwendung
$status = (new CircuitBreaker('gateway'))
    ->call(fn () => $gateway->paymentStatus($paymentId));
```

Für Queue-Jobs gibt es in Laravel bereits ein naheliegendes Pendant – die Middleware `ThrottlesExceptions`: Nach einer festgelegten Anzahl von Exceptions verschiebt sie die nächsten Versuche, statt einen nicht funktionierenden Dienst weiter zu bombardieren:

```php
// app/Jobs/SyncGameRounds.php
use Illuminate\Queue\Middleware\ThrottlesExceptions;

public function middleware(): array
{
    // 10 Exceptions → 5 Minuten Pause (in Laravel 11+ ist das zweite Argument in Sekunden)
    return [(new ThrottlesExceptions(10, 5 * 60))->by('provider-games')];
}
```

> [!NOTE]
> **Der Zustand muss geteilt sein.** Liegt der Fehlerzähler im Speicher des Prozesses, hat jeder der 50 Worker seinen eigenen Stromkreis, und der Anbieter bekommt 50 × Threshold Requests, bevor alle geöffnet sind. Speichern Sie den Zustand in Redis.

---

<a id="bulkheads"></a>
## Isolation: eigene Queues für jeden Anbieter

Selbst mit Timeouts kann ein langsamer Anbieter alle Worker einer gemeinsamen Queue belegen, und Bestätigungsmails für die Registrierung warten, bis die mit Aufrufen des Zahlungsanbieters verstopfte Queue wieder frei ist. Das **Bulkhead-Muster (Schotten)** isoliert die Ressourcen:

```php
// app/Jobs/CapturePayment.php
public function __construct(public readonly int $paymentId)
{
    $this->onQueue('payments-gateway');
}
```

```ini
; /etc/supervisor/conf.d/queue-payments.conf
[program:queue-payments-gateway]
command=php /var/www/app/current/artisan queue:work redis --queue=payments-gateway --timeout=60
numprocs=4
```

Jeder Anbieter hat seine eigene Queue und sein eigenes Prozesslimit. Wenn ein Anbieter lahmt, stauen sich nur seine Jobs.

---

<a id="limitations"></a>
## Wo der Ansatz an seine Grenzen stößt

* **Nicht alle Anbieter unterstützen Idempotenzschlüssel.** Dann bleibt als einziger Schutz der Abgleich: Vor einem Retry beim Anbieter die Liste der Operationen zu Ihrer `order_id` oder `reference` abfragen. Gibt es nicht einmal eine solche API, ist ein Retry einer Geldoperation ohne manuelle Prüfung unzulässig.
* **Schlüssel sind nur begrenzt gültig.** Bei Stripe zum Beispiel 24 Stunden. Ein Retry nach zwei Tagen ist eine neue Operation.
* **Ein Circuit Breaker kann Sie auch vor einem gesunden Anbieter „schützen“.** Ein zu niedriger Schwellenwert öffnet den Stromkreis schon wegen ein paar zufälliger Fehler. Wählen Sie Schwellenwert und Zeitfenster anhand realer Fehlerstatistiken, nicht aufs Geratewohl.
* **Die Komplexität wächst.** Status `unknown`, Abgleich-Jobs, Webhook-Journal – das ist Code, der getestet und überwacht werden muss. Für die Anbindung einer Wetter-API auf der Startseite reichen ein Timeout und ein Cache; alles andere ist für Geld und Bestellungen gedacht.

---

<a id="common-mistakes"></a>
## Häufige Fehler

**1. Standard-Timeout.**
30 Sekunden Wartezeit pro Request machen aus der Störung eines Anbieters einen Ausfall der gesamten Seite. Setzen Sie `connectTimeout` und `timeout` explizit.

**2. Abbuchung nach einem Timeout wiederholen.**
Ein Timeout bedeutet „unbekannt“, nicht „Fehler“. Ohne Idempotenzschlüssel kann ein Retry das Geld ein zweites Mal abbuchen.

**3. 4xx-Antworten wiederholen.**
Ein fehlerhafter Request wird beim zweiten Versuch nicht korrekt. Wiederholen Sie nur Verbindungsfehler, `429` und `5xx`.

**4. Retries auf drei Ebenen gleichzeitig.**
HTTP-Client, Job und aufrufender Service multiplizieren die Anzahl der Requests. Wählen Sie eine Ebene.

**5. Duplikatprüfung per `SELECT` statt per UNIQUE.**
Zwei parallele Requests sehen beide „kein Datensatz vorhanden“ und führen beide die Operation aus. Schutz bietet nur ein Unique-Index in der Datenbank.

**6. Circuit Breaker im Speicher des Prozesses.**
Jeder Worker öffnet den Stromkreis für sich allein. Speichern Sie den Zustand in einem gemeinsamen Store.

**7. Kein Abgleich.**
Zahlungen im Status `unknown` hängen ewig, und die Nutzer schreiben an den Support. Ein zeitgesteuerter Abgleich ist Pflicht.

---

<a id="checklist"></a>
## Checkliste

1. Jeder externe Aufruf hat explizite Werte für `connectTimeout` und `timeout`, abgestimmt auf die Anzahl der Worker.
2. Lange Aufrufe sind aus dem HTTP-Request des Nutzers in eine Queue ausgelagert.
3. Wiederholt werden nur idempotente Operationen und nur Verbindungsfehler, `429` und `5xx`, mit exponentieller Pause und Jitter.
4. Geldoperationen erhalten vor dem Aufruf einen Idempotenzschlüssel; der Schlüssel wird mit UNIQUE-Constraint gespeichert und an den Anbieter übergeben.
5. Ein Timeout versetzt die Operation in `unknown`, danach startet der Abgleich.
6. Webhooks werden idempotent verarbeitet: Event-Journal mit UNIQUE `(provider, event_id)`.
7. Abbuchungen vom internen Guthaben laufen unter `lockForUpdate()`.
8. Der Circuit Breaker speichert seinen Zustand in Redis; für Jobs wird `ThrottlesExceptions` verwendet.
9. Jeder Anbieter hat eine eigene Queue und ein eigenes Prozesslimit.

---

## Zusammenfassung

Eine externe API ist keine Funktion, sondern eine verteilte Transaktion mit einem unzuverlässigen Teilnehmer. Timeouts, Retries und Circuit Breaker begrenzen den Schaden durch seine Ausfälle, während Idempotenz und Abgleich dafür sorgen, dass aus Ausfällen keine doppelten Abbuchungen werden. Wenn Sie nach einem Ausfall des Anbieters die Frage „Ist die Zahlung durchgegangen?“ beantworten können, ohne in dessen Dashboard nachzusehen, ist die Integration richtig entworfen.

---

<a id="self-test-quiz"></a>
## Selbsttest-Quiz

### Frage 1: Ein Abbuchungs-Request ist mit einem Timeout abgebrochen. Was ist richtig?
- A) Die Abbuchung sofort wiederholen: Ein Timeout bedeutet, dass kein Geld abgebucht wurde.
- B) Die Zahlung auf den Status `unknown` setzen und das Ergebnis über eine Statusabfrage beim Anbieter ermitteln (oder mit demselben Idempotenzschlüssel wiederholen).
- C) Die Zahlung als fehlgeschlagen markieren und den Nutzer bitten, erneut zu bezahlen.

<details>
<summary><b>Antworten anzeigen</b></summary>

**Antwort: B**
Ein Timeout sagt nichts darüber aus, ob die Operation ausgeführt wurde. Ein blinder Retry (A) oder eine erneute Zahlung durch den Nutzer (C) können zu einer doppelten Abbuchung führen. Sicher ist nur, entweder den Status zu prüfen oder den Request mit demselben Idempotenzschlüssel zu wiederholen, damit der Anbieter das Ergebnis des ersten Versuchs zurückgibt.
</details>

### Frage 2: Warum schützt die Prüfung „Gibt es bereits eine Zahlung mit diesem Schlüssel?“ per `SELECT` vor dem `INSERT` nicht vor Duplikaten?
- A) `SELECT` ist langsamer als ein Unique-Index.
- B) Zwei parallele Requests können gleichzeitig sehen, dass kein Datensatz existiert, und beide die Operation ausführen. Zuverlässig schützt nur eine UNIQUE-Constraint in der Datenbank.
- C) Laravel cacht die Ergebnisse von `SELECT`.

<details>
<summary><b>Antworten anzeigen</b></summary>

**Antwort: B**
Zwischen Prüfung und Einfügen liegt ein Zeitfenster für eine Race Condition. Ein Unique-Index wird vom DBMS selbst atomar geprüft, daher bekommt der zweite `INSERT` eine `UniqueConstraintViolationException`, und der Code gibt die bereits existierende Operation zurück.
</details>

### Frage 3: Warum speichert man den Zustand eines Circuit Breakers in Redis und nicht im Speicher des Prozesses?
- A) Redis ist schneller als der Arbeitsspeicher des Prozesses.
- B) Damit alle Worker und Nodes denselben Zustand des Stromkreises sehen und gleichzeitig aufhören, Requests zu senden – statt jeder für sich nach seiner eigenen Fehlerserie.
- C) Laravel kann keine Daten im Speicher des Prozesses halten.

<details>
<summary><b>Antworten anzeigen</b></summary>

**Antwort: B**
Bei lokalem Zustand muss jeder der Dutzenden Worker den Fehlerschwellenwert selbst erreichen, und der nicht funktionierende Anbieter bekommt ein Vielfaches an Requests. Ein gemeinsamer Zustand öffnet den Stromkreis sofort für das gesamte System.
</details>