The saga pattern is a design pattern for coordinating that kind of operation without a distributed transaction. It breaks the work into local transactions that each commit on their own, and pairs every one with a compensating action that reverses it when a later step fails. Isolation is the one guarantee it can't offer.

This article covers how sagas are coordinated, what that missing isolation costs in production, and when a shared database removes the need for the pattern altogether.

Key takeaways:

- The saga pattern breaks one business transaction into a series of local ones, giving each a compensating action that undoes it when a later step fails.

- Sagas give you atomicity, consistency, and durability, but not isolation, so anything reading concurrently can see a saga mid-flight.

- Two-phase commit stalls every participant when its coordinator crashes, and plenty of managed databases and brokers don't support it at all.

- Choreography-based sagas need a transactional outbox, because a crash between the local commit and the event publish strands the saga with nothing to recover it.

- Most teams reach for sagas because of a service split they didn't need, and a shared database with ordinary transactions makes the whole pattern go away.

## [Copy link to heading](#what-is-the-saga-pattern)What is the saga pattern?

The saga pattern is a way of running a long-lived business transaction as a sequence of local database transactions, each committing independently and each paired with a compensating transaction that reverses it. Formalized by [Garcia-Molina and Salem](https://www.cs.cornell.edu/andru/cs711/2002fa/reading/sagas.pdf) in a 1987 paper, it guarantees one of two outcomes. Either every step completes, or the steps that have already run are undone in reverse.

The pattern exists because some operations can't fit inside one database transaction. Placing an order touches order creation, payment capture, inventory reservation, and shipment scheduling. When each of those is owned by a different service with its own database, no single transaction spans them, and consistency moves out of the database and into your application code.

### [Copy link to heading](#how-the-saga-pattern-differs-from-two-phase-commit)How the saga pattern differs from two-phase commit

The two approaches split on what happens when something crashes halfway through. Two-phase commit (2PC) is a protocol for committing one transaction across multiple databases at once, all or nothing.

A coordinator collects YES votes from every participant, then broadcasts a commit or abort decision. If the coordinator crashes between those two moments, every participant is stuck holding locks, unable to commit or abort on its own because either choice might contradict the decision it never received. They wait for a coordinator that may never come back.

A saga has no equivalent moment to get stuck in, because every step has already been committed by the time the next one starts.

The two approaches diverge on what they're willing to give up under partial failure:

| Dimension | Two-phase commit | Saga pattern |
| --- | --- | --- |
| Atomicity | Atomic across all participants | Per-step, with compensation restoring a consistent end state |
| Coordinator failure | Participants block while holding locks | No global lock to hold, so the run resumes or compensates |
| Isolation | Provided by participant locks for the transaction's duration | Not provided, so intermediate states are visible |
| Undo mechanism | Protocol-level abort, invisible afterward | Application-level compensating transaction, visible in the audit log |
| Cloud availability | Requires XA support in every participant | Works across vendors that share no transaction manager |

The last row is the one that decides real architectures. Two-phase commit only works if every participant speaks XA, the standard interface that lets an outside transaction manager drive a commit across separate systems. Managed cloud services largely don't implement it.

Aurora PostgreSQL write forwarding lists `PREPARE TRANSACTION`, `COMMIT PREPARED`, and `ROLLBACK PREPARED` as unsupported. DynamoDB transactions can't reach past a single AWS account and region. Kafka keeps its exactly-once guarantees inside its own transactional API, so your database can't join a Kafka transaction.

Once an operation spans two vendors, 2PC isn't an option you weighed and rejected. It was never available, and a saga is what's left.

### [Copy link to heading](#why-the-saga-pattern-trades-isolation-for-availability)Why the saga pattern trades isolation for availability

Isolation is the guarantee a saga gives up. A database transaction normally hides its in-progress state from everyone else until it commits or rolls back. A saga can't do that, because it's really a chain of separate transactions, each one visible to the rest of the system the moment it commits.

Anything else reading that data mid-saga can see a step's result before the saga finishes, and act on it before a later compensation has a chance to undo it.

That produces three anomalies your application code now has to handle:

- Lost updates: Two sagas write the same record, and when the first one compensates, it restores the old value and erases what the second one wrote.

- Dirty reads: One saga reads a record another saga has written but not yet finished, so the value it acts on may be reversed moments later.

- Non-repeatable reads: A saga reads a value, makes a decision on it, then finds that the value has changed by the time a later step runs.

Compensation is also weaker than it sounds. A database rollback restores the prior state atomically and leaves no trace, while a compensation approximates that state with a new forward write. The original operation stays in the audit log alongside its reversal.

For a refund, that's correct behavior. The customer doesn't care that the charge and the reversal both exist in the ledger. For something the customer directly experienced, like a confirmation email or a status change they already acted on, a forward-only undo isn't enough on its own. Plan for that at design time, not after it shows up in an incident.

## [Copy link to heading](#core-components-of-a-saga-pattern-implementation)Core components of a saga pattern implementation

Every saga implementation, whatever framework runs it, assembles the same five parts.

### [Copy link to heading](#local-transactions,-the-pivot,-and-retryable-steps)Local transactions, the pivot, and retryable steps

Not every step in a saga carries the same recovery options. Steps fall into three categories, and where a step sits in the sequence determines what you can still undo. Compensable transactions can be semantically undone.

The pivot transaction is the point of no return, and once it commits, compensation is off the table. Retryable transactions follow the pivot and must be idempotent, because the only remaining path is forward until they succeed.

Ordering steps by category is a design decision with real consequences. Placing an irreversible external call late in the sequence keeps compensation available for as long as possible. Placing it early means most of the saga has no way back.

### [Copy link to heading](#compensating-transactions)Compensating transactions

A compensating transaction is a new operation that semantically reverses a committed step. Releasing a reserved inventory hold, refunding a captured payment, and returning consumed credits are all compensations.

They carry two requirements that are short to state and routinely missed in implementation. Compensations must be idempotent, because the recovery path can run more than once. And a compensation should only be registered after its forward step succeeds, since compensating work that never happened produces its own inconsistency.

Compensations belong in durable units with their own retry behavior rather than in a catch block as cleanup code. A compensation that fails silently leaves the system in exactly the state the saga was meant to prevent.

### [Copy link to heading](#orchestration-and-choreography-as-coordination-models)Orchestration and choreography as coordination models

Sagas are coordinated one of two ways. With orchestration, a central coordinator issues commands and tracks saga state. With choreography, each service commits locally, publishes an event, and downstream services react.

The choice determines where your debugging pain lands:

| Dimension | Orchestration | Choreography |
| --- | --- | --- |
| Control model | Central coordinator issues commands | Each service reacts to the previous step's event |
| Visibility | One coordinator owns global state | State distributed across services |
| Benefit | Traceable, and avoids cyclic dependencies | No single point of failure |
| Key drawback | The coordinator is itself a failure point | Global state is hard to trace |

Orchestration is the more common production choice. Under choreography, reconstructing why a saga stalled means correlating logs across every participant, and that cost grows with each service added to the flow.

### [Copy link to heading](#the-transactional-outbox)The transactional outbox

One structural constraint sits under every choreography-based saga. Each step must publish its event atomically with its local database commit. If the process crashes after the commit but before the publish, the event is lost and the saga stalls with no automatic recovery.

A transactional outbox closes that window by writing the outgoing event into the same local transaction as the business data, with a separate relay publishing it to the broker afterward. Treating the outbox as optional gives a choreographed saga a reliability hole it can't self-heal.

Event sourcing is the structural alternative, since an append-only log makes every state change publishable and removes the dual write at the storage layer. It's also a major architectural commitment that changes how state is stored, queried, and evolved, so it earns its place mainly when the event log has first-class product value.

### [Copy link to heading](#idempotent-consumers)Idempotent consumers

Every message broker worth using delivers at least once, which means every saga step handler will eventually receive a duplicate. Handlers that increment counters, capture payments, or send notifications produce visible damage when they run twice.

Deduplication has to live outside the handler. In-memory maps only hold within a single process instance, so on any platform where instances come and go, the deduplication store has to outlive the function, and a message that never deduplicates cleanly needs a [quarantine destination](https://vercel.com/i/dead-letter-queue) rather than an unbounded retry. Designing around at-least-once delivery starts with a working grasp of [API idempotency](https://vercel.com/i/what-is-idempotency).

## [Copy link to heading](#how-vercel-workflows-runs-the-saga-pattern-without-an-orchestrator)How Vercel Workflows runs the saga pattern without an orchestrator

[Vercel Workflows](https://vercel.com/docs/workflows) provides the durable execution layer a saga needs, with the coordination expressed in application code rather than a separate orchestration service. Marking a function with `"use workflow"` makes it durable, and marking each unit of work with `"use step"` checkpoints makes it automatic.

Workflows reached [general availability](/blog/a-new-programming-model-for-durable-execution) in April 2026, and since the October 2025 beta, it has processed over 100 million runs and over 500 million steps across more than 1,500 teams.

### [Copy link to heading](#durable-execution-replaces-the-coordinator-you'd-otherwise-operate)Durable execution replaces the coordinator you'd otherwise operate

Orchestration solves the traceability problem and introduces an availability one, because the coordinator is a long-lived process you now run, scale, and keep healthy. Building your own usually means maintaining a worker fleet, a status table, and retry logic as a distributed system sitting on top of the application it coordinates.

Vercel Workflows remove the separate orchestrator. Each step runs as its own function invocation, [Vercel Queues](https://vercel.com/docs/queues/concepts) enqueues the next one reliably, and managed persistence stores the event log that records every step input, output, and error. If a process crashes mid-run, the log replays and execution resumes from the last completed step rather than starting over.

Determinism is the constraint that makes replay safe, so the SDK intercepts `Math.random()` and `Date.now()` and keeps workflow functions sandboxed from the network and filesystem. That sandbox is a real restriction on how orchestration code gets written, and existing coordination logic that reads a clock or calls out mid-flow has to move into steps before it will run at all.

### [Copy link to heading](#compensating-transactions-as-durable-rollback-steps)Compensating transactions as durable rollback steps

Compensation logic written as inline cleanup in a catch block inherits none of the durability of the steps it's undoing. If the process dies during rollback, the partial compensation is lost, and the saga ends in a state neither committed nor reversed.

The [documented rollback pattern](https://workflow-sdk.dev/docs/foundations/errors-and-retries) treats compensations as steps in their own right. Each one then retries and persists like any forward step:

```
// Forward steps
async function reserveInventory(orderId: string) {
  "use step";
  // ... call inventory service to reserve ...
}

async function chargePayment(orderId: string) {
  "use step";
  // ... charge the customer ...
}

// Rollback steps
async function releaseInventory(orderId: string) {
  "use step";
  // ... undo inventory reservation ...
}

async function refundPayment(orderId: string) {
  "use step";
  // ... refund the charge ...
}

export async function placeOrderSaga(orderId: string) {
  "use workflow";

  const rollbacks: Array<() => Promise<void>> = [];

  try {
    await reserveInventory(orderId);
    rollbacks.push(() => releaseInventory(orderId));

    await chargePayment(orderId);
    rollbacks.push(() => refundPayment(orderId));

    // ... more steps & rollbacks ...
  } catch (e) {
    for (const rollback of rollbacks.reverse()) {
      await rollback();
    }
    // Rethrow so the workflow records the failure after rollbacks
    throw e;
  }
}
```

Each compensation registers only after its forward step succeeds, and the reversed iteration runs them in the order a saga requires. [Flora](https://vercel.com/customers/how-flora-shipped-a-creative-agent-on-vercels-ai-stack) uses Vercel Workflows to orchestrate more than 50 image models, with steps that persist and retry on failure.

### [Copy link to heading](#retries-and-failure-classification-at-the-step-boundary)Retries and failure classification at the step boundary

The distinction between compensable and retryable transactions only matters if your runtime can act on it. Uniform retry behavior forces the choice into application code, where a transient rate limit and a permanent validation error get handled by the same branch.

Vercel Workflows classifies failures at the step boundary. Steps retry up to 3 times by default, for 4 total attempts, and `maxRetries` adjusts that per step. Throwing `FatalError` skips retries entirely, which is the correct response to a 404 or a rejected payload, while throwing `RetryableError` with a `retryAfter` value sets the delay for a rate-limited downstream service.

`getStepMetadata()` exposes the current attempt number on top of that, so a retryable step past the pivot can apply [exponential backoff](https://vercel.com/i/exponential-backoff) without you writing a scheduler.

### [Copy link to heading](#idempotency-keys-derived-from-a-stable-step-identity)Idempotency keys derived from a stable step identity

Retries reintroduce the duplicate-side-effect problem that a saga was supposed to contain. A payment step that retries after a lost confirmation response charges the customer twice, and no amount of compensation logic fixes a charge you didn't know happened.

Every step invocation has a `stepId` that stays the same across retries and is globally unique. That makes it a correct idempotency key for third-party calls:

```
import { getStepMetadata } from "workflow";

async function chargeUser(userId: string, amount: number) {
  "use step";
  const { stepId } = getStepMetadata();

  await stripe.charges.create(
    {
      amount,
      currency: "usd",
      customer: userId,
    },
    {
      idempotencyKey: stepId,
    }
  );
}
```

The same identifier works for compensations, which need idempotency for the same reason. Deriving the key from a timestamp or a random value defeats the purpose, because a retry then presents a key the downstream API has never seen.

### [Copy link to heading](#at-least-once-delivery-without-an-outbox-to-operate)At-least-once delivery without an outbox to operate

Choreographed sagas carry the outbox as permanent infrastructure, including a relay process, a polling interval, and a table that needs its own monitoring. All of it exists to close one crash window between a local commit and an event publish.

Under orchestration on Workflows, that window closes differently. Step results and the continuation to the next step both land in the same managed event log, so there's no separate publish that can drift from the recorded state.

Vercel Queues handles delivery underneath with at-least-once semantics, retries, and visibility timeouts, and an idempotency key on publish deduplicates a retried send for the lifetime of the original message.

This doesn't eliminate every dual write. A step that writes to your own database and then crashes before reporting its result will re-execute, which is why steps still need to be idempotent. What it removes is the broker-publish gap and the infrastructure you'd otherwise run to cover it.

## [Copy link to heading](#when-to-use-the-saga-pattern-instead-of-a-shared-database)When to use the saga pattern instead of a shared database

A saga makes sense once an architecture has already committed to database-per-service and the coordinated operations cross a vendor boundary where 2PC isn't available. Most teams never reach that point, so the first question is which side of it you're on.

### [Copy link to heading](#most-saga-adoptions-trace-back-to-a-split-that-wasn't-needed)Most saga adoptions trace back to a split that wasn't needed

The saga is usually a symptom of an earlier decision, a service split the team didn't need at its scale. Reverse that split back into a shared database and the saga has nothing left to coordinate, because ACID transactions, meaning atomic, consistent, isolated, and durable, handle the consistency the saga was built to fake.

The compensation logic, the isolation anomalies, and the outbox all go with it. For a team of five to fifty engineers, that consolidation is usually the [right call](https://vercel.com/i/microservices-vs-monolith-saas).

### [Copy link to heading](#when-coordination-genuinely-can't-be-collapsed)When coordination genuinely can't be collapsed

Some splits are real, and no shared database can absorb them. A payment flow that crosses a payment processor, a fraud service, and your own order database has three separate systems by necessity, not by choice.

Media generation pipelines and AI agent workflows are the same, calling third-party services that can't join any transaction you control, where a mid-run failure may demand a refund or a quota return. When the boundary is genuinely external, the saga is earned, and the only remaining question is which layer runs it.

## [Copy link to heading](#run-the-saga-pattern-on-durable-infrastructure)Run the saga pattern on durable infrastructure

The order that commits while the payment times out is a real problem, and compensating transactions are a real answer to it. They're also a permanent tax on every operation that touches the flow, paid in isolation anomalies, countermeasures, and recovery code that only runs on the worst day.

Getting this right starts with checking whether the services involved need separate databases at all. When the answer is yes, the coordination machinery belongs in the platform rather than in a second system you maintain.

Here's what Vercel provides for teams running coordinated operations across service and vendor boundaries:

- Vercel Workflows: Durable execution with automatic step checkpointing through the `"use workflow"` and `"use step"` directives, so replay resumes from the last completed step after a crash rather than re-running the saga from the beginning.

- Durable rollback steps: Compensations written as steps get the same persistence and retry behavior as forward work, which keeps recovery from failing silently at the moment it matters most.

- Step-level failure classification: `FatalError`, `RetryableError` with a configurable `retryAfter`, and per-step `maxRetries` let compensable and retryable transactions behave differently without custom branching.

- Stable step identity: `getStepMetadata()` returns a `stepId` that survives retries, giving every external call and compensation a correct idempotency key.

- Vercel Queues: At-least-once delivery with retries, visibility timeouts, and publish-time deduplication, running underneath workflows without a broker or relay to operate.

[Start a new project](https://vercel.com/new) and ship a durable workflow on your first `git push`, or adapt a [multi-step template](https://vercel.com/templates) to your own services.

## [Copy link to heading](#frequently-asked-questions-about-the-saga-pattern)Frequently asked questions about the saga pattern

### [Copy link to heading](#what-is-the-difference-between-saga-orchestration-and-choreography)What is the difference between saga orchestration and choreography?

Orchestration uses a central coordinator that tracks saga state and issues commands to each participant. Choreography distributes coordination through events, with each service reacting to the previous step's output. Orchestration gives clearer visibility into the global state, while choreography avoids a single point of failure at the cost of harder tracing.

### [Copy link to heading](#what-is-a-compensating-transaction)What is a compensating transaction?

A compensating transaction reverses the business effect of a step that has already been committed, such as refunding a captured payment or releasing an inventory hold. It runs as a new write rather than a rollback, so it must be idempotent and can only be registered once its forward step has succeeded.

### [Copy link to heading](#can-you-implement-the-saga-pattern-on-a-serverless-platform-like-vercel)Can you implement the saga pattern on a serverless platform like Vercel?

Yes. Vercel Workflows supplies the durable execution layer with step isolation, automatic retries, deterministic replay after a crash, and sleep that holds no compute. The `"use workflow"` and `"use step"` directives cover the infrastructure, while application code still owns the compensation logic and business rules.

### [Copy link to heading](#when-should-you-use-a-modular-monolith-instead-of-the-saga-pattern)When should you use a modular monolith instead of the saga pattern?

A modular monolith fits when the services involved would share a database anyway, since ACID transactions remove the need for compensation entirely. The same holds when a team is small enough that service boundaries add more coordination overhead than they remove.