Comparison
How nestjs-durable compares to Temporal, Inngest, and BullMQ — what the suspend-model library approach buys you, and when a dedicated orchestration cluster or a managed platform is the better call.
Durable execution is a crowded space, and the honest answer to "which one?" is it depends on your
architecture. This page positions nestjs-durable against the three tools people most often weigh
it against — as fairly as we can manage.
The short version: nestjs-durable is a library, not a platform. It lives inside your NestJS
app, persists state to the SQL database you already run through the ORM you already use, and
dispatches steps over the queue you already have. There is no extra server to deploy, no vendor to
sign up with, and no new language runtime — the trade being that you own scaling and operations,
the way you already do for the rest of your app.
At a glance
| nestjs-durable | Temporal | Inngest | BullMQ | |
|---|---|---|---|---|
| Shape | NestJS library | Orchestration cluster + SDKs | Managed platform (or self-host) | Queue library |
| Extra infra | none — your DB + optional queue | Temporal server + its DB | Inngest server / cloud | Redis |
| State lives in | your Postgres/MySQL/SQLite | Temporal's persistence | Inngest's store | Redis |
| Workflow model | replay + suspend, plain async code | replay, plain code | step memoization over HTTP | jobs + flows (no replay) |
| Waits (sleep/signal) | zero-compute suspend | zero-compute | zero-compute | delayed jobs only |
| Sagas (compensation) | built-in — { compensate } on any step | manual try/catch pattern | manual | — |
| Event triggers | onEvent + debounce/batch | schedules; signal-with-start by hand | core feature (debounce, batch) | — |
| Per-key mutex | singleton (FIFO per key) | one run per workflow id | concurrency key = 1 | Pro only (groups) |
| Durable entities | built-in (@Entity/@On) | — | — | — |
| Cross-language | TS + Python workers, one workflow | per-language SDKs | HTTP (any language) | JS/TS only |
| DI integration | native NestJS providers | manual | framework-agnostic | manual |
| Dashboard | built in | Temporal Web | Inngest dashboard | third-party (Taskforce, Bull Board) |
| Scale ceiling | your DB's write throughput | very high (dedicated cluster) | managed for you | Redis throughput |
vs Temporal
Temporal is the reference implementation of durable execution, and its model — deterministic replay
of plain code — is the same one this library uses. If you've written a Temporal workflow,
ctx.step / ctx.sleep / signals / ctx.patched will all feel familiar; even the
query/update split is deliberately Temporal-shaped. A few things Temporal leaves as hand-rolled
patterns are first-class here: saga compensation is a { compensate }
option on any step (undone in reverse, durably) instead of a try/catch you write yourself, and
durable entities — keyed, serialized actors — have no Temporal
equivalent at all.
The difference is operational. Temporal is a separate orchestration cluster: its own server,
its own database, its own deployment lifecycle, workers connecting over gRPC. That buys enormous
scale, multi-region namespaces, and battle-tested guarantees — and costs you a second distributed
system to run (or a Temporal Cloud bill). nestjs-durable inverts the trade: the engine is a
module in your app, state is rows in your existing database, and "deploying the orchestrator" is
just deploying your app.
Choose Temporal when workflow orchestration is a core, high-scale concern — millions of concurrent runs, many teams, many languages — and a dedicated cluster earns its keep. Choose nestjs-durable when you're a NestJS shop that wants durable checkout/onboarding/pipeline flows without adopting and operating a second platform.
vs Inngest
Inngest attacks the same problem from the serverless direction: your steps are memoized function invocations driven over HTTP by the Inngest server, which makes it a natural fit for Vercel/Netlify-style deployments and event-driven fan-out. It is excellent at what it does, and its managed offering removes the ops question entirely.
The differences that matter here: nestjs-durable workflows are long-lived class instances with
NestJS dependency injection — steps are provider methods, tested like any provider — and the
suspend model parks a waiting run as a database row rather than re-invoking a function per step.
State stays in your database (a hard requirement in regulated or air-gapped environments), and
step dispatch rides your own transport (in-process, BullMQ, SQS, or plain DB polling) instead of
inbound HTTP. Inngest's signature event ergonomics have direct analogues too:
@Workflow({ onEvent, debounce, batch }) starts workflows from published
events with the same coalescing patterns ("reindex at most once a minute", "process uploads in
batches").
Choose Inngest for serverless/edge architectures or when you want a managed platform with event-driven ergonomics. Choose nestjs-durable for long-running NestJS services, DI-first codebases, and data-residency constraints.
vs BullMQ
This is the comparison that matters most in practice, because BullMQ is what most NestJS apps already use — and this library runs happily on top of BullMQ as its production transport.
BullMQ gives you jobs and queues: retries, priorities, rate limits, delayed jobs, and flows for parent/child trees. What it does not give you is a durable function: there is no replay, so a multi-step process must be modeled as a chain of independent job handlers passing state through job data. The "what happened so far" lives implicitly across queues; a crash between jobs leaves you reconstructing progress; waiting on a human approval means inventing a persistence scheme for the half-finished flow.
nestjs-durable keeps BullMQ underneath for what it's great at — moving step invocations between
processes — and adds the durable layer on top: the whole flow is one readable function, every step
is checkpointed, waits are await ctx.sleep(...) / await ctx.waitForSignal(...) that survive
deploys, and one dashboard shows the end-to-end run.
Choose plain BullMQ for fire-and-forget background jobs — a resize, an email, a webhook delivery — where each job is independent and "workflow" would be overkill. Choose nestjs-durable the moment a process has steps that must not re-run, waits, or state that outlives a deploy.
Where nestjs-durable is honestly weaker
- Scale ceiling. The engine writes checkpoints to your SQL database. That's a feature (one source of truth, your backup story) until your run volume makes checkpoint writes a hot spot — a dedicated cluster like Temporal moves that ceiling far higher.
- Ecosystem maturity. Temporal has a decade of production hardening and a large community; this library is younger and NestJS-scoped by design.
- Languages. Cross-language support today means TypeScript workflows with Python workers (or Python-authored workflows over the same protocol) — not the per-language SDK breadth of Temporal.
If those are your constraints, use the bigger tool. If they aren't, skipping a second platform is a real simplification — in code, in ops, and in the audit story.
Getting Started
Run your first durable workflow in an existing NestJS app — install the module, write a workflow, register it, and start a run. Zero infrastructure with the event-emitter transport.
Concepts
The mental model behind the engine — why replay is the durability mechanism, what a workflow and a step actually are, how a run waits for hours without holding a process, and whose runs a pool executes.