Skip to content

Workers & scaling

Every process that calls AddPgWorkflows is a worker. Call DisableWorkers() and it’s a client. That’s the whole model.

  • A worker leases and executes workflows and activities.
  • A client starts, signals, and awaits workflows — and never executes anything.

Postgres is the only coordination layer. No scheduler service, no leader election, no message broker.

There is no worker setup. AddPgWorkflows registers a hosted background worker, so the app that defines your workflows processes them — an ASP.NET API, a console app, and a Windows service are all equally valid workers.

builder.Services.AddPgWorkflows(pg =>
pg.UsePostgres(connectionString)
.AddWorkflow<TrialOnboardingWorkflow>()
.AddActivities<EmailActivities>()
);

To scale, deploy more instances. Leases in Postgres make this safe:

  • Two workers never run the same step (FOR UPDATE SKIP LOCKED).
  • Leases are heartbeated while work runs, so slow work isn’t stolen.
  • A dead worker’s lease expires; a peer resumes the run from the last completed step.
  • A worker that lost its lease has its writes rejected — no stale-worker corruption.

A front-facing API shouldn’t compete for work — it should dispatch and move on.

// API — pure client, runs no workers
builder.Services.AddPgWorkflows(pg =>
pg.UsePostgres(connectionString)
.DisableWorkers()
.AddWorkflow<TrialOnboardingWorkflow>()
);

Starting a workflow is a single INSERT — cheap enough for your hottest request path. Whichever worker leases the run first executes it.

For high-throughput APIs, prefer fire-and-forget: StartAsync, return the run id, let callers check back. Awaiting GetResultAsync per request polls the database — fine for tens of waiters, not a million.

Put workflows and activities in a shared class library; every participating process registers from it:

MyApp.Workflows/ ← workflow + activity classes
MyApp.Api/ ← client: AddWorkflow + DisableWorkers
MyApp.Worker/ ← worker: AddWorkflow + AddActivities

Clients need AddWorkflow (to resolve names and types) but can skip AddActivities — activities only matter where they execute.

All knobs and defaults are in the configuration reference. Two worth knowing early:

  • WorkerId defaults to the machine name — set it explicitly in containers so leases are attributable when debugging.
  • Activity MaxConcurrency defaults to four per processor (IO-friendly); lower it for CPU-bound work.