long-running operation

rule

an LRO is the asynchronous request-reply contract for work that cannot finish within one HTTP request: the command durably creates or reuses an operation resource, returns its location, and the client polls it until the owning state machine reports a terminal state.

the pair is indivisible:

  • an LRO without a state machine has no authoritative status semantics.
  • a state machine alone does not define acceptance, polling, retry guidance, or terminal delivery.

consequence of skipping this pattern:

  • returning a provider-owned or executor-specific identifier (e.g., a workflow instance ID) as the status breaks all clients when the executor is replaced.
  • clients lose a durable operation identity and cannot safely retry after network loss or timeout.
  • status stored independently from facts: an interruption between effect and status update leaves the state machine and visible state permanently out of sync; observers read stale or inconsistent status across retries.
  • each executor becomes an implicit partial copy of the state machine logic; disagreement compounds across clients and replicas.

why

asynchronous work outlives the request that started it, so the client needs a durable handle. a provider-owned identifier as the product resource couples every client to a replaceable executor; status stored apart from facts lets an interruption split what happened from what is visible. a durable operation resource with state derived from the owning state machine gives:

  • a safe retry identity;
  • a stable polling contract;
  • a terminal answer that survives network loss.

acceptance contract

before acknowledging work, the command boundary:

  1. validates input, identity, and authorization;
  2. adopts a caller idempotency key or derives one from a stable command identity;
  3. creates or reuses one durable operation ID;
  4. commits operation intent and a dispatch claim atomically;
  5. dispatches the replaceable executor or leaves a claim that reconciliation can dispatch safely.

accepted work returns 202 Accepted: Location names the canonical polling resource, Retry-After gives the first polling delay, and the body repeats the durable identity and current semantic state.

HTTP/1.1 202 Accepted
Location: /operations/op_01
Retry-After: 3
Content-Type: application/json
{
"operationId": "op_01",
"state": "accepted",
"statusUrl": "/operations/op_01"
}

acceptance means the command is durable, not that execution has started or will succeed. if the service cannot create recoverable intent, it returns a typed failure instead of 202.

polling resource

GET statusUrl returns 200 OK while the operation exists. state is always derived by the owning state-machine fold over canonical facts, never copied from an executor SDK.

the domain owns the exact closed state set; every state is terminal or nonterminal:

  • nonterminal representations may include updated retry guidance;
  • terminal success includes a typed result or a stable link to it;
  • terminal failure includes a safe typed problem;
  • absence, expiry, and lost authorization use ordinary typed HTTP failures and never fabricate progress.
type OperationRepresentation<Result, Problem> = {
operationId: string;
state: OperationState;
statusUrl: string;
result?: Result;
problem?: Problem;
};

result is legal only for terminal success, problem only for terminal failure. prefer a discriminated union when the contract language can express it.

idempotency, dispatch, and reconciliation

  • repeating one command with the same idempotency key returns the same operation ID and status URL — never starts a second logical operation merely because the first HTTP response was lost.
  • execution claims are durable and bounded. a reconciler finds accepted work whose dispatch is absent or expired, claims it idempotently, and dispatches again.
  • executors commit canonical outcome facts before freshness events or notifications; the state machine then derives the terminal result.

client contract

the polling client:

  • follows Location or statusUrl rather than constructing provider URLs;
  • honors Retry-After, then uses bounded exponential backoff with jitter;
  • stops on every terminal state and on lost authorization or expiry;
  • tolerates repeated representations and transient transport failures;
  • does not infer success from elapsed time or HTTP acceptance.

cancellation, when the domain supports it, is a separate authorized command: it commits cancellation intent; the state machine decides whether that intent produces cancelling, cancelled, or a conflict with an already terminal outcome.

identifiers and provider boundary

  • operationId identifies the client-visible durable operation resource.
  • correlationId joins logs and traces across one execution path.
  • neither name substitutes for the other.
  • provider workflow IDs, queue message IDs, job handles, and provider status are internal execution facts: recordable for diagnosis and reconciliation, but never polled by API consumers or treated as semantic operation state.
  • replacing the executor must not change the operation URL or state contract.

verification

  1. lose the acceptance response, retry the command, and receive the same operation ID.
  2. interrupt after intent commit but before dispatch, then prove reconciliation starts the work once.
  3. interrupt after outcome commit but before notification, then prove polling returns the terminal state.
  4. return every nonterminal and terminal representation through the generated client contract.
  5. replace or fake the executor and prove no provider identifier enters the public response.
  6. poll at the advised cadence and prove the client stops at every terminal state.

good and bad

bad

{
"workflowInstanceId": "provider-7842",
"status": "running"
}

a replaceable executor becomes the product resource; status semantics sit outside the domain contract. clients must construct provider URLs to poll, lose any safe retry identity after a lost response, and break in concert the moment the executor is replaced or upgraded.

good

{
"operationId": "op_01",
"state": "accepted",
"statusUrl": "/operations/op_01"
}

one durable product resource; the owning state machine derives every state from durable facts. a lost acceptance response retries into the same operation identity, polling survives network loss, and swapping the underlying executor changes nothing the client can see.

search pages

go to any page