Priority and Fairness

When more flows are ready to run than the Foreman has workers to execute, something has to decide what runs next. By default every flow is treated equally, which is fine until an urgent flow is stuck behind a long batch job, or one busy tenant’s flood of work crowds out everyone else. Microbus gives you two dials to control this: priority and fairness. Both are set per flow through workflow.FlowOptions at create time — you never tune the scheduler itself.

Priority

Priority decides which flow runs first. An explicit priority is an integer 1 or greater, where lower numbers run first — a flow at priority 1 is dispatched ahead of a flow at priority 5. Leaving it at 0 (the zero value) is not priority zero; it means “unset”, and the flow takes the Foreman’s DefaultPriority config instead.

Priority is strict and has no aging: as long as higher-priority work keeps arriving, lower-priority flows wait. This is intentional — it lets you guarantee that an interactive, user-facing flow is never delayed by background batch work. Choose your numbering with that in mind: reserve the low numbers for genuinely urgent flows and let bulk work default.

A flow that doesn’t set a priority uses the Foreman’s DefaultPriority config (default 5), so the common pattern is to leave most flows unset and only mark the few that need to jump the queue:

import "github.com/microbus-io/dwarf/workflow"

// Expedite this flow ahead of default-priority work.
flowID, err := foremanapi.NewClient(svc).Create(ctx, myserviceapi.CreditApproval.URL(),
    map[string]any{"applicant": applicant},
    &workflow.FlowOptions{Priority: 1},
)

Fairness

Priority alone doesn’t stop a single tenant from monopolizing the workers — if one customer submits ten thousand flows at the default priority, everyone else’s default-priority flows wait behind them. Fairness solves this by sharing dispatch within a priority level across fairness keys.

A fairness key is just a label that groups related flows — most often a tenant or customer ID. Among flows of the same priority, the Foreman rotates dispatch across the distinct fairness keys rather than draining them in submission order, so a tenant with a huge backlog gets its turn but not the whole pool. Crucially, a key’s share does not grow with its backlog: submitting more work does not buy more throughput, which is what keeps one noisy tenant from starving the rest.

If you don’t set a fairness key, the Foreman derives one from the request’s tid or tenant actor claim when present, and otherwise places the flow in a shared default bucket. In a multi-tenant service where calls already carry a tenant claim, fairness works correctly with no code changes.

Set it explicitly when the grouping isn’t the tenant claim — for example, fairness per end-user within a tenant:

&workflow.FlowOptions{FairnessKey: "user-" + userID}

Fairness Weight

By default every fairness key gets an equal share. FairnessWeight changes a key’s relative share: a key with weight 2.0 receives roughly twice the dispatch share of a key at the default weight of 1.0, when both have work waiting. Use it for tiered service levels — say, premium tenants getting a larger slice than free-tier ones:

&workflow.FlowOptions{FairnessKey: tenantID, FairnessWeight: 2.0}

Weight only matters relative to the other keys competing at the same priority; a key with no waiting work consumes none of its share.

Setting Flow Options

workflow.FlowOptions is passed when a flow is created. All fields are optional — a nil *workflow.FlowOptions, or any zero-valued field, falls back to the Foreman’s defaults.

type FlowOptions struct {
    Priority       int           // >= 1, lower runs first; 0 means unset → DefaultPriority
    FairnessKey    string        // groups flows; empty derives from the tenant claim
    FairnessWeight float64       // relative share of the key; 0 means weight 1
    TimeBudget     time.Duration // per-task time budget for this flow; 0 uses the engine default
    ThreadKey      string        // join an existing thread; empty starts a fresh one
}

These fields are frozen at Create and immutable for the life of the flow. Priority, FairnessKey, FairnessWeight, and TimeBudget are inherited unchanged by Continue, Fork, and subgraphs. ThreadKey is the explicit-policy way to add a turn to an existing thread — pass any flow key already in the thread, and the new flow joins it while you set its scheduling explicitly. (Continue is the inherit-everything convenience that copies the thread’s policy; the two can be mixed.) A flow whose start should be deferred is authored in the workflow itself — an entry task that calls flow.Interrupt, released by Resume — not via a scheduling option.

TimeBudget overrides the Foreman’s default per-task time budget for every ExecuteTask dispatch in this flow — a per-task ceiling, not a flow-wide deadline — and bounds each task’s context. It is frozen at Create. A task’s own sub.TimeBudget endpoint declaration still applies; the effective deadline is the smaller of the two.

A workflow is invoked by submitting it to the Foreman. Pass the options as the last argument to Create or Run (use nil when you want the defaults):

client := foremanapi.NewClient(svc)

outcome, err := client.Run(ctx, workflowURL, initialState,
    &workflow.FlowOptions{Priority: 1, FairnessKey: tenantID},
)

The same final argument applies to client.Create when you start and await the flow separately.

Operating Notes

  • Defaults live on the Foreman. DefaultPriority is a foreman.core config; raise or lower it to shift where “unmarked” flows sit relative to your expedited and batch tiers.
  • Priority is soft across replicas. Each Foreman replica schedules independently, so ordering is honored closely but not as a single global queue. Treat priority as “strongly preferred”, not a hard real-time guarantee.
  • Observability. The Foreman exports metrics for pending steps and their age per priority band, and the number of distinct fairness keys in play, so you can see whether a low-priority tier is being starved or a tenant is dominating. These feed the bundled Grafana dashboard.

Further Reading