DEV Community

Cover image for AWS Step Functions vs Camunda for Sagas: Two Orchestrators, Different Blast Radii
Andrii B.
Andrii B.

Posted on

AWS Step Functions vs Camunda for Sagas: Two Orchestrators, Different Blast Radii

Here's a trap I've watched smart teams walk into. They pick a saga orchestrator by comparing the two engines feature-for-feature, decide they both "do sagas," flip a coin weighted by whatever's already in their cloud bill, and move on. Then a year later one team is tuning Zeebe partitions at 2am and the other is staring at a Step Functions bill that grew faster than their traffic, both wondering how the "same pattern" turned into such different jobs.

They do run the same pattern. A saga is a sequence of local transactions where, if step four fails, you run compensating actions to undo steps one through three, because you don't have a distributed transaction to roll back for you. Both AWS Step Functions and Camunda 8 are orchestration-style saga coordinators: a central brain that knows the steps, drives them in order, and triggers the rollback when something breaks. That part is genuinely the same.

What's not the same is what you're on the hook for. We run Camunda 8 in production for credentialing and scheduling workflows, so I've lived one of these; the other I've read the docs on and priced out for real proposals. The honest comparison isn't "which has more features." It's "which failure is yours to own." Let me lay both out.

The saga both of them run

Orchestration sagas need a coordinator because someone has to remember where the process is. Choreography (services reacting to each other's events with no central brain) works until you need to answer "why is order 8842 stuck," and then nobody can, because the state lives smeared across six services' logs. An orchestrator centralizes that: one place holds the process state, drives the next step, and owns the compensation logic.

So the shape is identical on both. Take a classic order saga: reserve inventory, charge the card, ship. If the charge fails, release the inventory. If shipping fails, refund the charge and release the inventory. Three forward steps, a compensation for each, run in reverse. The pattern is old and boring, which is the point. The interesting part is what each engine makes you do to express it, and what each one does when the wheels come off.

The same order saga (reserve inventory, charge payment, ship) modeled two ways: an AWS Step Functions JSON state machine wiring each Catch to a compensation task, beside a Camunda BPMN flow with compensation boundary events

Step Functions: the orchestrator AWS operates for you

Step Functions is a managed state machine. You describe your saga in Amazon States Language, which is JSON, and AWS runs the thing. There are no brokers to operate, no cluster to size, no exporter to babysit. For a lot of teams that single sentence is the whole pitch, and it's a good one.

You get retries and error routing as declarative config. Here's the reserve-then-charge slice, with a retry on the flaky step and a Catch that routes a failed charge to the compensating action:

{
  "StartAt": "ReserveInventory",
  "States": {
    "ReserveInventory": {
      "Type": "Task",
      "Resource": "arn:aws:states:::lambda:invoke",
      "Parameters": { "FunctionName": "reserveInventory" },
      "Retry": [
        { "ErrorEquals": ["States.TaskFailed"], "MaxAttempts": 3, "IntervalSeconds": 1, "BackoffRate": 2.0 }
      ],
      "Next": "ChargePayment"
    },
    "ChargePayment": {
      "Type": "Task",
      "Resource": "arn:aws:states:::lambda:invoke",
      "Parameters": { "FunctionName": "chargePayment" },
      "Catch": [
        { "ErrorEquals": ["States.ALL"], "Next": "ReleaseInventory" }
      ],
      "Next": "Ship"
    },
    "ReleaseInventory": {
      "Type": "Task",
      "Resource": "arn:aws:states:::lambda:invoke",
      "Parameters": { "FunctionName": "releaseInventory" },
      "Next": "SagaFailed"
    },
    "SagaFailed": { "Type": "Fail", "Error": "SagaFailed" }
  }
}
Enter fullscreen mode Exit fullscreen mode

Look at what compensation actually is here: it's you, by hand, pointing each step's Catch at the right cleanup state and remembering to undo everything that already succeeded. There's no "compensate the whole saga" primitive. If Ship fails, its Catch has to route to a state that refunds the payment AND then to a state that releases the inventory, in the right order. Miss one and you've got a half-rolled-back order and no error to tell you. The state machine is honest and explicit, which is a nice way of saying verbose and entirely your responsibility.

For long-running sagas you use Standard Workflows, and this is where the surprise lives. Standard Workflows bill per state transition. Every step your machine takes is a transition: a task, a wait, a choice. The first 4,000 a month are free, then it's $0.000025 each. Cheap, until you read the next sentence: every retry attempt is its own billed state transition. So the exact moment your dependencies are flaky and your saga is retrying and compensating, which is the moment you most need it to work, is also the moment it's transitioning the most and billing the most.

Do the math on a bad month. Say the order saga is six states on the happy path. Add a payment step that burns its three retries and then compensates two prior steps: call it eleven transitions for that execution instead of six. Now imagine a million sagas in a month running hot like that:

1,000,000 sagas x ~11 transitions = ~11,000,000 state transitions
minus 4,000 free
x $0.000025
= ~$275 / month, in Step Functions transitions alone
(Lambda invocations, and everything the tasks actually do, billed separately)
Enter fullscreen mode Exit fullscreen mode

That number is small until it isn't. The point isn't the dollar figure, it's the shape of the curve: your orchestration cost scales with steps and with failures, not with business value. A retry storm is a billing event. For high-volume, short-lived sagas AWS pushes you to Express Workflows, which bill on requests plus duration plus memory instead, but Express caps how long a saga can run and changes the durability guarantees, so it's a different tool, not a cheaper Standard.

Human steps and long waits work through the callback pattern: a task pauses with a token and resumes when something calls back with that token. It's a clean primitive. It is also, again, plumbing you assemble. There's no built-in inbox, no task list, no assignment or escalation. If your saga has a "a human approves this" step, Step Functions gives you the pause and the resume and leaves the actual human workflow to you.

Camunda 8: the orchestrator you operate

Camunda flips almost every one of those tradeoffs. You model the saga in BPMN, and compensation is a first-class construct: you attach a compensation boundary event to each activity, define its handler (the undo), and when something throws, one compensation throw event fires all the relevant handlers, in reverse, automatically. You don't hand-route rollbacks. You declare "charging the card is compensated by refunding it" once, on the task, and the engine does the reverse walk. Camunda's own framing is that Zeebe acts as the saga coordinator that solves the transaction without two-phase commit, and the compensation modeling is the part that makes that pleasant instead of a pile of Catch states.

The workers are your code. A Zeebe worker subscribes to a task type and does the real work; throwing a BPMN error is what triggers the compensation path:

import { ZBClient } from "zeebe-node";

const zbc = new ZBClient();

zbc.createWorker({
  taskType: "charge-payment",
  taskHandler: async (job) => {
    try {
      await payments.charge(job.variables.orderId, job.variables.amount);
      return job.complete();
    } catch (err) {
      // A BPMN error, not a crash: the modeled compensation boundary
      // events upstream fire and unwind the saga for us.
      return job.error("PAYMENT_FAILED", "card declined");
    }
  },
});
Enter fullscreen mode Exit fullscreen mode

Two things Step Functions makes hard, Camunda makes boring. First, human tasks are native: user tasks are part of the model, and since 8.5 you drive them through the Zeebe REST API, with a task list, assignment, and forms as real product surface rather than something you glue on. For our credentialing flows, where a human genuinely has to review and approve, that's not a nice-to-have, it's the reason the engine earns its keep.

Second, and this is the one people underestimate until it bites: in-flight process versioning. When you fix a bug in a saga that has thousands of instances mid-flight, deploying version two doesn't disturb the running ones; they finish on the version they started on. When you actually need to move them, you migrate in-flight instances, through Operate or the Zeebe API, and you can canary a new version against real traffic. A saga that spans days or weeks will outlive several deploys of its own definition. Having a real answer to "we changed the process while ten thousand of them were running" is the difference between a workflow engine and a fancy job queue.

The bill for all this is that you operate it. Even on Camunda's SaaS you're closer to the machine; self-managed, you're running Zeebe brokers, choosing a partition count, and feeding an exporter into Elasticsearch so Operate has something to show. Zeebe is a distributed, log-based engine, every state change is an append to a replicated log, which is exactly why it recovers cleanly and exactly why it's a real distributed system with your name on the pager. Nobody at AWS gets paged when your exporter falls behind.

Operational-ownership comparison: Step Functions is mostly operated by AWS with cost and lock-in as your blast radius, while self-managed Camunda 8 is mostly operated by you (Zeebe brokers, partitions, Elasticsearch exporter)

Where the blast radii actually differ

Both engines can lose you a saga. What differs is which failure domain is yours.

With Step Functions, the engine's blast radius is AWS's problem. If a broker equivalent falls over, that's their incident, their multi-AZ replication, their 3am. Your blast radius moved somewhere else: into cost, which grows with every retry, and into lock-in, because Amazon States Language and the deep .sync and callback integrations don't travel. The day you want to run the same saga off AWS, you're not porting a config, you're rewriting the orchestrator. That's a real blast radius, it's just a slow one that shows up in a migration quarter instead of a pager.

With Camunda, the engine's blast radius is yours. A misconfigured partition count, an exporter that can't keep up, a botched broker upgrade, those are your incident. In exchange, you own the model. The saga is BPMN you can read, version, and migrate; the compensation is declared, not wired; the engine runs wherever you put it, on any cloud or none. Your blast radius is operational and immediate, but it's also fully in your hands, and it doesn't have a per-transition meter running.

Neither of these is the "safe" choice. They're different bets on which kind of failure you'd rather own: someone else's engine with your cost and lock-in exposure, or your own engine with your operational exposure.

The four questions that actually decide it

Skip the feature matrix. Four questions settle it in practice.

How gnarly is your compensation? If your rollback is "undo the last thing, maybe two," Step Functions' hand-wired Catch states are fine and the managed simplicity wins. If your saga has real compensation depth, conditional undo, partial rollbacks, compensation that itself can fail, Camunda's first-class compensation modeling stops being a nicety and starts being the thing that keeps the logic legible.

Are there humans in the saga? A step where a person reviews, approves, or fills in a form tilts hard toward Camunda. Step Functions can pause for a callback, but you're building the entire human side. Camunda ships it.

How long do instances live, and will the definition change under them? A saga that completes in seconds rarely outlives its own code. One that runs for days or weeks will, and in-flight versioning goes from "feature I skimmed" to "the reason we didn't have an outage on deploy." That's Camunda's home turf.

Who operates it, and what happens to the bill at scale? If you have no appetite to run a distributed engine and you're already all-in on AWS, Step Functions' managed model is a genuine gift, right up until the transition bill on a high-retry, high-volume workload makes someone ask why orchestration costs more than the work it orchestrates. If you have the operational muscle and you care about portability or predictable cost, running Camunda is a real option rather than a burden.

Here's my actual position, from the side I've lived: for the workflows we run, credentialing and scheduling, with human approvals, multi-day instances, and compensation that has to be right, Camunda was never a close call, and the operational tax has been worth every cent of not owning the model. But I've also priced Step Functions for a stateless, short-lived, no-humans, all-on-AWS saga and recommended it without flinching, because there the managed simplicity is the whole point and the transition bill stays small. The mistake isn't picking either one. The mistake is picking it because it "does sagas," and finding out a year later which blast radius you actually signed up for.


Originally published at andriiboyko.com.
If you found this helpful, follow me here and on LinkedIn

Top comments (0)