DBRaven
Failure Mode · consistency

Saga Compensation Cascade

critical

Summary

When a distributed saga times out mid-execution and the compensating transactions for already-completed steps cannot be executed because the target services are also unavailable, the system is left in an irrecoverable intermediate state with no automated resolution path. The saga coordinator marks the saga as "compensating" but cannot drive it to a terminal state, resulting in indefinitely stuck business objects (orders, payments, reservations) requiring manual intervention.

Description

The saga pattern achieves distributed transaction semantics through a sequence of local transactions, each with a defined compensating transaction that semantically reverses the effect. The safety guarantee holds only if all compensating transactions can be executed successfully. When a compensation fails, the saga cannot reach a terminal state (committed or fully rolled back), leaving the system in a partially-committed intermediate state that may be invisible to both the initiating service and the end user.

The failure scenario: an order placement saga executes steps 1 (inventory reserve), 2 (payment charge), 3 (fulfillment notify). Step 2 times out after 30 seconds. The saga coordinator initiates compensation: C2 (void payment authorization), C1 (release inventory reservation). C2 is sent to the payment service. If the payment service is experiencing degraded availability (which may be why step 2 timed out in the first place), C2 also times out. The compensation saga is now stuck: the inventory is still reserved (C1 has not run because C2 must succeed first in an ordered saga), and the payment authorization may or may not have been charged (the original timeout left the outcome ambiguous).

The idempotency requirement compounds the problem. For compensation to be safe to retry, each compensating transaction must be idempotent. If C2 (void payment) is not idempotent, retrying it risks double-voiding a payment that was already voided, which may generate a second credit transaction. Production implementations frequently have idempotency gaps in compensating transactions because they are rarely tested in failure scenarios.

The stuck saga accumulates over time. In a system processing 1,000 orders per hour with a 0.1% compensation failure rate, 1 saga gets stuck per hour. After 24 hours, 24 stuck sagas hold reserved inventory, have ambiguous payment states, and require manual review. Each stuck saga represents a customer-visible failure (order in "pending" state indefinitely) and a potential financial discrepancy.

Characteristics

Propagationlinear
Time to detect5–30 minutes via saga state monitoring (alert on sagas in "compensating" state for >5 minutes). Without explicit saga state monitoring, detection occurs when customers escalate stuck orders, typically 30–120 minutes after the incident.
Blast radiusEach stuck saga locks business resources (inventory, payment authorization, reservation slots) for the duration of the stuck state. At scale, this produces ghost reservations that are never released, causing false inventory unavailability and customer-visible order failures. The financial impact includes charged-but-not-fulfilled payments, voided-but-already-fulfilled payments, or both, depending on which step timed out and whether compensation partially succeeded.

Triggers

  • ·Compensating service is unavailable when compensation is triggered (same failure that caused the original step to time out)
  • ·Saga step timeout threshold too short for external payment or logistics provider latency variance
  • ·Compensation transaction is not idempotent, causing retry logic to abort after first attempt fails
  • ·Saga coordinator itself crashes during compensation execution, losing in-flight compensation state
  • ·Network partition between saga coordinator and compensation target during the compensation window

Detection Signals

alertlog errorserror rate spike

Mitigation Strategies

Durable saga state with persistent compensation queuecomplexity: high

Store all saga state transitions in a persistent store (outbox table in PostgreSQL, or a dedicated saga log). Use the outbox pattern to enqueue compensation messages: compensation steps are written to an outbox table atomically with the saga state update, then polled and delivered by a reliable outbox consumer. If the saga coordinator crashes, the outbox consumer restarts and re-delivers unacknowledged compensation messages. This makes compensation delivery at-least-once and durable across coordinator failures.

Mandatory idempotency keys for all compensating transactionscomplexity: medium

Every compensating transaction must accept an idempotency key (saga_id + step_id) and deduplicate on it at the target service. The target service checks if the compensation was already applied (SELECT 1 FROM compensation_log WHERE idempotency_key = $1) before executing, and returns success if it was. This makes compensation retries safe and allows the saga coordinator to retry stuck compensations indefinitely without risk of double-application.

Saga timeout tuning with external provider SLA marginscomplexity: low

Set saga step timeouts to 3x the p99 latency of the target service SLA (e.g., if payment provider p99 is 2 seconds, set step timeout to 6 seconds). Monitor external provider latency separately from saga timeout configuration. Reduces the frequency of timeout-triggered compensation without increasing end-user wait time beyond acceptable bounds for most requests.

Recovery Steps

  1. 1.Query the saga state store for all sagas in "compensating" state with age > 10 minutes
  2. 2.For each stuck saga, determine which compensation steps have succeeded and which are pending
  3. 3.Manually execute compensation steps that cannot be retried automatically, using the idempotency key to check if already applied
  4. 4.For ambiguous payment states (timed-out charge), contact payment provider to determine actual charge status via their idempotency API
  5. 5.After resolving, mark the saga as "failed" or "compensated" in the state store and release any held reservations
  6. 6.Add alert on sagas in compensating state > 5 minutes if not already present

Estimated recovery time: 30 minutes to 4 hours per stuck saga batch depending on the number of affected sagas and whether payment state reconciliation with external providers is required. Automated retry with idempotent compensations should resolve most cases within 5–10 minutes.

Affected Systems

Patterns

saga patternoutbox patternevent sourcing

Technologies

postgresqlkafkarabbitmq

Basis

Saga compensation failure mechanics are well-documented in distributed systems literature (Garcia-Molina 1987, Richardson 2018); stuck compensation accumulation rate is derived from realistic failure rate estimates; idempotency requirement is a known production implementation gap