Durable Ruby Workflow Tooling Comparison

Ductwork is a durable workflow engine for Ruby on Rails. It runs multi-step pipelines inside your app and keeps all of the workflow state in the database you already run (PostgreSQL, MySQL, or SQLite), so there's no separate cluster or broker to stand up. Think DBOS-style durable execution, Ruby-native, without the determinism and replay constraint.

Comparison at a glance

Tool Language Workflow state lives in Extra infrastructure Workflow shape Determinism / replay constraint Exactly-once advancement License and pricing Rails-native
Ductwork Ruby Your database (Postgres, MySQL, SQLite) None DAG: fan-out/fan-in, parallel branches, conditional routing None Yes LGPL-3.0 open source; Pro $200/mo Yes
ActiveJob::Continuation Ruby The job's own payload in your queue backend None (Rails 8.1+) Linear steps inside one job None, but code outside steps re-runs N/A (one job) MIT (part of Rails) Yes
acidic_job Ruby Your database None Linear steps inside one job None, but steps must be idempotent N/A (one job) MIT Yes
Sidekiq Pro batches Ruby Redis Redis, Sidekiq Pro license Fan-out, then a callback None No, callbacks are regular jobs Commercial, $99/mo or $995/yr Yes
Temporal Polyglot, Ruby SDK GA Temporal Service Temporal cluster or Temporal Cloud Arbitrary code: child workflows, parallel activities Workflow code must be deterministic for replay Yes, via event history MIT; Temporal Cloud is usage-based No
Camunda Java-centric, clients in other languages Camunda's Zeebe engine Camunda 8 cluster or SaaS BPMN diagrams Modeled in BPMN, not code Yes, engine-managed Camunda License; production self-managed needs an enterprise license No
DBOS Python, TypeScript, Go, Java Postgres None besides Postgres Code: workflows calling steps Workflow function must be deterministic Yes, via step checkpoints MIT; paid cloud and management tools No
Hatchet Python, TypeScript, Go, Ruby (pre-1.0) Hatchet engine's Postgres Hatchet engine, self-hosted or Hatchet Cloud DAGs and durable tasks Durable tasks must be deterministic between checkpoints; none stated for DAG tasks Yes, engine-managed MIT; Hatchet Cloud is paid No
Oban Pro Workflows Elixir Your database (Postgres, MySQL, SQLite) None DAG: dependencies, fan-out/fan-in, sub-workflows None Not documented; jobs are inserted together and held until their dependencies finish Oban is Apache 2.0; Pro $150/mo No

What are we actually comparing?

In The Atomicity Gap I walked through what happens when a background job crashes between two of its lines. A job with more than one side effect repeats the first one on retry (Double Work). A job that enqueues the next job can commit its own work and lose the enqueue (Lost Work). On a hard crash, both happen silently. Every tool in this post is, one way or another, an answer to that problem.

First, a distinction that trips people up. Sidekiq, GoodJob, and Solid Queue are job runners. They're very good at taking a job, running it, and retrying it when it raises. What they don't own is the handoff between jobs. When job A finishes and job B should start, that's your code calling perform_later, and that's exactly where the gap lives. A workflow orchestrator owns the handoff.

Here's what I looked at for every tool below, so the sections line up:

  • Durability model: what actually happens if the process gets SIGKILLed mid-step?
  • Workflow topology: linear only, or branching and fan-out/fan-in?
  • Programming constraints: do you have to write deterministic code, think about replay, or follow special rules for side effects?
  • Operational footprint: does it live in your database, or is it a separate cluster to run (or pay for)?
  • Idempotency support: does it help you make an external side effect, like a Stripe charge, safe to repeat?

One column in the table needs a definition: exactly-once advancement. That's the guarantee that moving a workflow from one step to the next happens once and only once, even through a crash. Without it, a pipeline can silently stop (Lost Work) or fork itself in two (Double Work). Running the step itself is a different story. Nobody can promise exactly-once when the side effect is an API call to someone else's server, so step execution is at-least-once, in Ductwork and everywhere else. The honest question is whether the tool helps you make that second run harmless. In Ductwork, every step gets a stable idempotency_key you can hand to Stripe and services like it, so a re-run after a crash doesn't charge anyone twice.

In the Ruby and Rails ecosystem

Ductwork vs ActiveJob::Continuation

ActiveJob::Continuation shipped in Rails 8.1. It lets a single job split its perform into steps, with an optional cursor inside each step, so an interrupted job picks back up after the last completed step instead of starting over.

class ImportJob < ApplicationJob
  include ActiveJob::Continuable

  def perform(import_id)
    @import = Import.find(import_id)

    step :validate do
      @import.validate!
    end

    step :process do |step|
      @import.records.find_each(start: step.cursor) do |record|
        record.process
        step.advance! from: record.id
      end
    end

    step :finalize
  end
end

It's honestly great at what it's for. It's built into Rails with zero new dependencies, and it's made for the long-running import or backfill that has to survive a deploy. When the queue adapter says it's stopping, the job checkpoints, re-enqueues itself with its progress, and resumes later.

Where it differs from Ductwork is scope. It's resumability within one job, not orchestration across many. There's no fan-out/fan-in, and handing off to another job is a plain perform_later, which puts you right back in the Lost Work gap. Progress only gets saved when the job is interrupted or retried, so a SIGKILL or OOM kill between checkpoints starts the job over, if your backend brings it back at all. And cursor correctness is on you. Advance the cursor before the side effect lands and a crash skips work. Advance it after and a crash repeats it.

Choose it if one long job needs to survive restarts and doesn't need to coordinate with any other jobs.

Ductwork vs acidic_job

acidic_job is the closest head-to-head in the Rails world. It's an MIT-licensed gem built on Active Job that runs a workflow as a series of steps inside one job. It records progress in your database, so a retry resumes at the step that failed instead of at the top.

The design advice in its README lines up with mine: each step should have at most one foreign side effect, and steps should be safe to run again. If you're already using acidic_job well, you've already shrunk your units of work.

The difference is shape. acidic_job workflows are linear. There's no fan-out/fan-in and no parallel branches, and since the 1.0 rewrite dropped awaits there's no first-class way to wait on other jobs either. It also leans on your job backend to do the retrying. If a crashed job never comes back (Sidekiq drops it, Solid Queue parks it as failed), nothing resumes it.

One more thing to know: 1.0 is still a release candidate (1.0.0.rc7, from January 2026), and its API is different from 0.9. Check which version any tutorial you find was written for.

Choose it if your workflows are strictly sequential, you want everything inside Active Job, and you're comfortable running a release candidate.

Ductwork vs Sidekiq batches (and GoodJob batches)

Sidekiq Pro batches let you enqueue a group of jobs and run a callback when they're done. complete fires when every job has run once, success fires when every job has succeeded, and death fires the first time one dies. Batches live in Redis, and Sidekiq Pro is $99/month or $995/year. GoodJob has the Postgres-backed equivalent in GoodJob::Batch, with on_finish, on_success, and on_discard callbacks.

Batches are a good primitive for "run these N things, then do one more thing." Where they fall short is chaining. A callback is just another job. When your callback writes to your database and then kicks off the next batch, that's a dual write between your database and Redis, the same Lost Work gap from before. Build a five-stage pipeline out of callbacks and you've got four of them. Your workflow's shape also ends up scattered across callback classes instead of in one place you can read top to bottom. And a heads up: pending Sidekiq batches expire after 30 days, and when they do their callbacks never run.

GoodJob is a little better off here. If GoodJob shares your app's database, the next enqueue can ride in the same transaction as your write, which closes that particular gap. You still don't get the rest of what a workflow engine gives you, like passing outputs from one step to the next, conditional routing, or a view of the whole pipeline.

Choose it if you already run Sidekiq Pro (or GoodJob) and all you need is a simple "run N, then call back."

Enterprise workflow engines

Ductwork vs Temporal

Temporal is event-sourced durable execution. The Temporal Service records every workflow's history, and when a worker dies the workflow gets rebuilt by replaying that history through your workflow code. It's polyglot, and its official Ruby SDK has been generally available since October 2025.

Temporal is the heavyweight on this list, and it earns it. It runs at massive scale, a Go service and a Ruby service can take part in the same workflow, the tooling is mature, and Temporal Cloud is there if you don't want to run it yourself.

The cost is the programming model and the footprint. Because recovery is replay, workflow code has to be deterministic. No direct I/O, no reading the clock, no random numbers, nothing that could come out differently the second time through. All of that moves into activities, and changing workflow code while runs are in flight means thinking about versioning. Then there's the cluster. You either run the Temporal Service yourself, plus its persistence database, or you pay for Temporal Cloud. That's a lot of machinery for "charge a card, then send a receipt."

Ductwork takes the other side of that trade. Steps are plain Ruby classes with no determinism rules. Recovery isn't replay: a step's output is saved when the step commits, and a crashed step just runs again. And all of it lives in your existing Rails database.

Choose it if you're multi-language, operating at very large scale, or already running Temporal.

Ductwork vs Camunda

Camunda is a business process orchestration platform. You model processes in BPMN, a standard diagram notation, and its Zeebe engine executes them. It's Java-centric and built so analysts and developers can work on the same process diagram, with strong support for human tasks.

That's a really different starting point. Camunda is diagram-first and Ductwork is code-first. Camunda is usually bought by an organization so business stakeholders can own and audit their processes. Ductwork is something a Rails developer adds to a Gemfile. Licensing is worth knowing about too: since 8.6, running Camunda 8 Self-Managed in production requires an enterprise license.

Ductwork Pro does have human-in-the-loop (dampen pauses a pipeline before a step until a person resumes it), but it isn't trying to be a BPMN tool.

Choose it if business analysts own the process diagrams, you need BPMN for compliance, or human tasks are the core of the process.

Durable execution beyond Ruby

Ductwork vs DBOS

DBOS is library-style durable execution backed by Postgres. You mark workflows and steps in your code, DBOS checkpoints each step's output to Postgres, and after a crash it re-runs the workflow, skipping every step that already has a checkpoint. It has SDKs for Python, TypeScript, Go, and Java.

DBOS is the closest philosophical analog to Ductwork. The database is the source of truth and there's no separate orchestrator to run. That's the whole idea behind Ductwork too.

There are two differences. The obvious one is that DBOS has no Ruby SDK. The other is that DBOS recovers by re-running your workflow function, so that function has to be deterministic, same as Temporal: given the same inputs and step outputs, it has to call the same steps in the same order. Ductwork doesn't re-run anything to figure out where it was. It picks up from the last committed step. Ductwork also brings the idea to Rails across Postgres, MySQL 8+, and SQLite, where DBOS is Postgres-only.

Choose it if you're writing Python, TypeScript, Go, or Java and you're already on Postgres.

Ductwork vs Hatchet

Hatchet is Postgres-backed task orchestration. A separate Hatchet engine owns scheduling, DAG state, and retries, and your app runs workers through a language SDK that connect to the engine over gRPC. It's MIT-licensed, and you can self-host the engine or use Hatchet Cloud.

Architecturally it's the closest thing to Ductwork here: Postgres underneath, DAGs, and workers you run yourself. The difference is where the state lives. From Ruby, your process runs workers that execute your task code, but scheduling, DAG state, and retries live in Hatchet's engine and Hatchet's database, not your app's. That's one more service to deploy, monitor, and upgrade. Ductwork doesn't need an extra service at all.

Hatchet also has two ways to model work, and they come with different rules. DAGs are declared up front, like a Ductwork pipeline. Durable tasks decide at runtime what to spawn and wait on, and they recover by replaying an event log, so the code between checkpoints has to be deterministic. They're also only supposed to wait or spawn child tasks, with database and API calls pushed into those children. That's the same trade Temporal makes, just scoped to durable tasks.

The Ruby SDK is official but young. The hatchet-sdk gem is pre-1.0 (0.10.0 as of October 2026) and it's moving fast, so check its feature parity with the Python, TypeScript, and Go SDKs for anything you plan to rely on.

Choose it if you're polyglot or already run Hatchet, and you're comfortable adopting a pre-1.0 Ruby SDK.

Ductwork vs Oban (Elixir)

Oban is database-backed job processing for Elixir. It started on Postgres and supports MySQL and SQLite now too. Oban Pro adds Workflows: jobs with declared dependencies, fan-out/fan-in, sub-workflows, and context shared between them.

I'm including it even though it's not Ruby, because it's Ductwork's closest sibling. Both put the queue in your database. Both are open core: Oban is open source and Workflows come with Pro at $150/month, and Ductwork is open source with a Pro tier at $200/month.

If you squint, Ductwork is what Oban Pro Workflows would look like for Rails. The main design difference is that Oban started as a job queue and grew workflows on top, while Ductwork started as a workflow engine with its own job system underneath. If you're on Elixir, use Oban. It's excellent.

Choose it if you're writing Elixir.

FAQ

Is there a Temporal alternative for Ruby on Rails?

Yes, and I'm obviously biased about which one. Ductwork gives you durable, multi-step workflows with the state in the database you already run, so there's no cluster to stand up and no determinism rules to learn. If your workflows are strictly linear, acidic_job works too. And if you actually want Temporal, its Ruby SDK has been GA since October 2025. Just budget for running it.

Does Sidekiq support workflows?

Not really. Sidekiq runs jobs. Sidekiq Pro batches get you "run these N jobs, then call back," and you can chain batches into something workflow-shaped. But every callback is a regular job, so each handoff is a database write plus a separate enqueue to Redis, and a crash between the two loses the next stage.

Can I run durable workflows without Redis?

Yes. Ductwork keeps everything in your relational database (Postgres, MySQL, or SQLite) and never touches Redis. acidic_job and ActiveJob::Continuation can skip it too, as long as your Active Job backend does. Solid Queue and GoodJob are both database-backed.

Is there a DBOS equivalent for Ruby?

Not from DBOS, which has no Ruby SDK. Ductwork is built on the same idea, the database as the source of truth, but it resumes from the last committed step instead of re-running your workflow function, so your code doesn't have to be deterministic.

How does Ductwork handle exactly-once processing?

Advancing from one step to the next is exactly-once, and running a step is at-least-once, with a stable idempotency_key per step so repeats are safe. More on that in What are we actually comparing?

Which should you choose?

  • If one long job needs to survive a deploy, use ActiveJob::Continuation.
  • If your workflow is strictly sequential and you want it all inside Active Job, use acidic_job.
  • If you need "run N jobs, then call back" and already pay for Sidekiq Pro, use Sidekiq batches. On GoodJob, use GoodJob batches.
  • If you're multi-language, at huge scale, or already running Temporal, use Temporal.
  • If analysts own the process and you need BPMN, use Camunda.
  • If you're on Python, TypeScript, Go, or Java with Postgres, use DBOS.
  • If you're polyglot and happy to run a separate engine, use Hatchet.
  • If you're on Elixir, use Oban Pro.
  • If you have a Rails app with multi-step pipelines, fan-out/fan-in, or anything that touches Stripe, and you don't want another cluster, use Ductwork.

And if every job you have does exactly one thing and none of them enqueue each other, you might not need any of this. Seriously.

Find Your Gaps

Not sure you need any of these tools yet? Start by finding out where the gaps are. I wrote a prompt that you can hand to your coding agent. It reads your jobs and reports every Atomicity Gap it finds, labeled as Double Work or Lost Work, with the file and lines it found it on. It's read-only, makes no network calls, and installs nothing. Copy the prompt or read it first.