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
BackgroundServiceorPeriodicTimer. 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
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.
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.
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.
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.
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.
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.
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.
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.
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.
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
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
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
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
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
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
- 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.
- 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.