Acta. Durable execution for .NET

Choosing

When Acta is the right choice, and when another tool is.

If the tool you run today already makes background work dependable and easy to see into, keep it. Acta is for work that has become application state: its steps, waits, retries, and history must survive a crash and stay in the database the application already uses.

Start from the kind of work

A local loop where a missed tick is harmless
BackgroundService or PeriodicTimer. Nothing needs to be stored, so nothing more is needed.
A script or container the platform starts on a calendar
cron, systemd timers, Windows Task Scheduler, a Kubernetes CronJob, or a cloud timer. The platform owns the clock and the process.
Messages between services
A message broker or a streaming platform, for pub/sub, fan-out, and streams you replay. Acta can run the work a message starts, with the message id as its deduplication key.
Background work the application owns
Acta, when the work needs retries, durable state, a history, or an operator who can pause, restart, or signal it.
A long-running workflow in .NET
Acta: one C# handler with steps, child jobs, signals, and timers, and its state as rows in your database.
Workflows across several languages, or drawn in a designer
A workflow platform. Acta is .NET only and has no visual designer or BPMN.

If you already run one of these

Hangfirebackground job runner

Keep it when it already runs your jobs well: a widely used runner with recurring jobs, automatic retries, continuations, and a dashboard, on storage you know, with batches and throttling in its paid packages.

Consider Acta when progress inside a job has to survive a restart: named steps, signals, long waits, and child jobs in one handler, with order and limits held across every worker and the job's state in SQL views you can query.

Quartz.NETscheduler

Keep it when the calendar is the hard part: rich cron and calendar rules, misfire handling, and clustering you have already tuned. Its 4.x line adds a dashboard, continuations, and execution groups that cap work across a cluster.

Consider Acta when the job itself has state to keep: steps whose results survive a restart, long waits, child jobs, lineage, and one answer to why a job is where it is.

TickerQscheduler

Keep it when you want a source-generated scheduler with persistence, a dashboard, priorities, and, in its 5.x line, durable chains of jobs with parallel branches.

Consider Acta when one job should carry its own progress: checkpoints inside the handler, signals and timers it waits on, lanes and limits, recovery evidence, and operator intervention.

Workflow Coreworkflow library

Keep it when your workflows are graphs of steps, declared in fluent C#, JSON, or YAML, and it already persists them where you need.

Consider Acta when you would rather write the workflow as an ordinary C# handler, with named steps, waits, and child jobs as its durable points, and run it under the same controls, limits, and SQL views as every other job.

Temporalworkflow platform

Keep it when workers in several languages share one workflow, you want deterministic replay of its event history, or you already run its server or use its cloud service.

Consider Acta when the workflow is .NET, its state belongs in the SQL database the application already uses, and you would rather not run a separate workflow server.

Elsa Workflowsworkflow library

Keep it when you want workflows as data that can change without a deploy, built in code or drawn in its designer, hosted in your application or on their own.

Consider Acta when your team writes and reviews workflows as plain C# handlers, and wants its jobs, schedules, and workflows in one model.

Durable TaskDurable Functions and its SDKs

Keep it when you host on Azure Functions, or want a managed Azure service to hold your orchestrations' state. Durable Functions can also keep it in SQL Server.

Consider Acta when the state belongs in the application's own PostgreSQL, SQL Server, or SQLite database, wherever the application runs, and you want checkpoints you place yourself rather than orchestrator code that replays.

Wolverinemessaging framework

Keep it when your background work is message handling: a durable inbox and outbox, durable local queues, and scheduled messages, on a database you already run.

Consider Acta when the unit is a job with a life of its own: steps, waits, child jobs, a schedule, a history, and an operator who can pause, restart, or signal it.

RabbitMQ, Kafka, Service Bus, SQSmessage broker

Keep it for messaging between services, those in other languages included, with whatever pub/sub, fan-out, or replay your broker or streaming platform offers.

Add Acta beside it when the work a message starts needs retries, steps, and a history: the broker carries the message, and Acta runs the work. Without a broker, Acta runs background work on the database alone.

Each row describes the project as its own documentation did in November 2026: Quartz.NET at 4.x, and TickerQ at 5.x, then a preview.

The same six questions, asked of each

The table above says when each tool is the right one. Here the same six questions are asked of Acta and of six of them, and each answer is the one the project's own documentation gives, with the page it comes from, as it read in November 2026. Where a project's documentation is silent, the answer says so.

Waiting hours or daysfor something outside the job

Acta The handler awaits ctx.WaitSignalAsync<Approval>("approval"), and jobs.RaiseSignalAsync releases it; SleepAsync and SleepUntilAsync are durable timers. The job holds no worker while it waits. guide

Hangfire A job is one method call. A later stage is another job, started by BackgroundJob.ContinueJobWith or delayed with BackgroundJob.Schedule; its documentation does not describe a wait for an outside event inside a running job. docs

Quartz.NET A continuation trigger (StartAfter) waits in the job store for its parent firing to end, and a trigger can be stored paused with a reason until someone resumes it. The wait is between jobs, not inside one. docs

Workflow Core The workflow definition calls .WaitFor("event", ...), released by host.PublishEvent; .Schedule and .Recur cover time. docs

Elsa A blocking activity (Delay, Timer, Event, UserTask) or context.CreateBookmark leaves a bookmark, and the workflow resumes when the bookmark is triggered. docs

Temporal A [WorkflowSignal] method sets state and the workflow awaits Workflow.WaitConditionAsync; Workflow.DelayAsync is the durable timer. docs

Durable Task The orchestrator awaits context.WaitForExternalEvent<T>("Approval"), raised by a client, and context.CreateTimer is the durable timer. docs

A finished step's outputwhere it lives, and what a restart does with it

Acta A row in steps, in the application's database, readable through steps_view. After a crash the handler runs again from its first line, and a step that succeeded returns its stored result without running its body. guide

Hangfire A job's return value is kept with its succeeded state in the job storage, and since 1.8 a continuation can take its parent's result. An interrupted job is retried from the start. docs

Quartz.NET The job's JobDataMap in the job store, saved after a run when the job asks for it; 4.x adds a run report to the execution history. docs

Workflow Core Steps map their output into the workflow's data object, which is persisted with the instance between steps in the configured provider. docs

Elsa In the instance's persisted workflow state and execution log, in the configured store; interrupted instances are requeued on startup. docs

Temporal An event in the workflow's event history, held by the Temporal Service. A worker replays the workflow code against that history, and a completed activity returns its recorded result. docs

Durable Task An event in the orchestration's history in the backend: the Durable Task Scheduler, or for Durable Functions also Azure Storage or SQL Server. The orchestrator replays against it. docs

Changing the codewhile work is outstanding

Acta New code takes effect the next time a job is entered, since the handler always runs from the top. Step, signal, timer, and child names are keys into stored state and keep their meaning, and stored payloads must stay readable. There is no recorded history the code has to replay consistently. guide

Hangfire A queued job names its method. Its documentation pages do not cover changing one; a post on hangfire.io advises adding a new method over changing the parameters, name, or type of one a queued job names. post

Quartz.NET A job type that can no longer be loaded puts its trigger in an error state until the stored class name is corrected, and stored job data must stay readable by the new code. docs

Workflow Core A definition has an id and a version, and an instance is started for one version. Its documentation does not describe changing a definition under running instances. docs

Elsa Definitions are versioned: publishing makes a new version and a running instance keeps its own. An alteration moves instances to a newer version. docs

Temporal Workflow code must stay deterministic against the recorded history. A change that running executions would replay needs patching (Workflow.Patched), worker versioning, or a new workflow type. docs

Durable Task Adding, removing, or reordering an orchestrator's calls breaks instances in flight. Orchestration versioning stamps each instance with a version the code branches on. docs

A limitshared by every worker, or one process's own

Acta A concurrency key and its limit, a rate limit, and a lane are held in the database, so each one holds across every worker. guide

Hangfire The worker count is per server. Mutexes, semaphores, and rate windows are in the paid Hangfire Ace package, which keeps their state in the job storage. docs

Quartz.NET The thread count is per node. An execution group caps a category of work per node, or across the cluster when given a cluster limit; a start rate is a limiter added as job middleware. docs

Workflow Core Its documentation describes no concurrency or rate setting. docs

Elsa Its documentation describes no cluster-wide cap on workflow concurrency; a distributed lock coordinates access to one instance across nodes. docs

Temporal A worker's concurrency and rate options are its own. MaxTaskQueueActivitiesPerSecond is enforced by the service across every worker of a task queue. docs

Durable Task Durable Functions' concurrency throttles apply to each worker. A Durable Task Scheduler task queue, a preview, has a throughput limit the scheduler enforces. docs

Why work is not movinghow an operator finds out

Acta jobs.ExplainAsync, <app> jobs explain, or the job's dashboard page says what the status means, what the job waits on, why the last attempt failed, and what an operator can do next. jobs_view and events_view answer the same from SQL. guide

Hangfire The dashboard shows a job's state history and its exception, and the monitoring API returns each state with its reason. docs

Quartz.NET The 4.x dashboard counts triggers by state, blocked, paused, and in error among them, and shows what is executing and the execution history. docs

Workflow Core Its documentation describes no UI. A step's error reaches the host's OnStepError event and the logs, and an Elasticsearch index can be searched by status. docs

Elsa Elsa Studio's instance viewer shows the status, incidents, and journal of an instance, and the REST API returns the same. docs

Temporal The Web UI shows a workflow's event history, pending activities, and call stack, and temporal workflow describe and show give them from the CLI. docs

Durable Task The Durable Task Scheduler dashboard shows an instance's timeline and history; Durable Functions adds an HTTP status API. docs

What runs beside the applicationfor more than one node

Acta The application's own PostgreSQL or SQL Server database, and nothing else; one process can run on SQLite. support

Hangfire A job storage: SQL Server, or Redis with the paid Pro package. docs

Quartz.NET A shared database behind its ADO.NET job store, with clustering on and the nodes' clocks within a second of each other. docs

Workflow Core A persistence database, plus a queue provider and a distributed lock manager. docs

Elsa A database, plus a distributed lock provider, a message bus for cache invalidation, and clustered scheduling. Its designer is optional. docs

Temporal A Temporal Service: self-hosted with its persistence and visibility stores, or Temporal Cloud. docs

Durable Task Durable Functions runs on Azure Functions with a storage account and a state backend. The standalone SDKs run anywhere and keep their state in the Durable Task Scheduler, a managed Azure service. docs

If an answer here is wrong or has gone out of date, that is a defect in this page: open an issue.

Signs that Acta fits, and signs that it does not

Acta fits when
  • The work must survive restarts, deploys, and crashed workers.
  • Someone will ask what ran, what failed, and why a job is stuck.
  • Operators need to pause, resume, cancel, restart, or signal jobs.
  • One customer's or one order's jobs must run in order, one at a time.
  • PostgreSQL or SQL Server already runs beside the app, or the app is one process on SQLite.
Choose something else when
  • Losing a tick is harmless and nobody will ask what happened.
  • The work is host maintenance that the OS or the platform owns.
  • Workers in more than one language share the orchestration.
  • You want local variables to survive a crash without naming what to keep. Acta runs the handler again from the top and skips the finished steps.
  • A duplicate external side effect cannot be made idempotent, guarded, or reconciled.