# API Contracts for Work That Outlives the Request

> Represent long-running work as a durable resource with stable identity, lifecycle states, cancellation, retries, retention, and results.

Canonical URL: https://www.devobs.io/articles/long-running-operation-api-contracts/
By: Theo Morgan
Published: 2023-12-19T10:02:49.261Z
Updated: 2026-09-05
Section: Architecture

When work takes longer than a reliable request window, return a durable operation resource rather than holding the connection open. Give it a stable identifier, explicit state machine, progress that clients can interpret, a terminal result or error, and defined cancellation and retention behavior.

## Return an operation, not a promise

After validating and accepting a request, create the operation and enqueue work atomically, then return `202 Accepted` with the operation representation and a URL for status. The operation should identify its type, creation time, owner, target resource, state, and safe retry information. If the request is rejected before acceptance, return the normal synchronous error and do not create a phantom operation.

Google's [AIP-151](https://google.aip.dev/151) defines a common long-running operation shape with a name, completion indicator, metadata, and either result or error. Even if your API is not a Google-style API, the separation between generic lifecycle fields and method-specific metadata is valuable. Clients can build one polling mechanism while each operation exposes relevant progress.

Use a small state machine: queued, running, succeeded, failed, cancellation_requested, and cancelled may be enough. State transitions must be monotonic except for a documented retry that creates a new attempt. Distinguish a failed operation from an unavailable status endpoint. Preserve terminal results long enough for clients to recover after disconnects.

## Make polling cheap and correct

Return validators such as ETags and support conditional GET so unchanged polls can receive a minimal response. Suggest a polling interval and apply jitter. Back off as operations age, and stop at terminal state. [RFC 9110](https://www.rfc-editor.org/rfc/rfc9110.html) defines `202 Accepted`, representation metadata, validators, and conditional request semantics that support this contract. Do not use `200` from the creation request to imply that the underlying work completed.

A progress percentage is useful only when the denominator is stable. For discovery-heavy work, expose phases or counters such as `files_scanned` instead of a percentage that moves backward. Keep diagnostic messages machine-readable through codes and structured details, with a short user-facing explanation. Do not expose stack traces or credentials.

## Define cancellation as a request

Cancellation is often best effort. `POST /operations/{id}:cancel` can move the operation to cancellation_requested; the worker then stops at a safe checkpoint. State what cannot be undone. If an external payment was already submitted, cancelling local polling does not reverse it; the workflow needs a compensating operation. Authorize cancellation independently and make it idempotent.

Retries need two layers. Retrying creation with the same idempotency key should return the same operation. Retrying a failed operation may create a new operation linked to the previous attempt, preserving history. Never reset a failed resource to queued if that erases the error a client is reconciling.

Callbacks are preferable when operations are rare or polling would be wasteful, but delivery becomes another asynchronous contract. Sign callbacks, include operation ID and event ID, retry with backoff, and expect duplicates. The callback tells the client to fetch authoritative state; it should not be the only retained result. Offer polling as the recovery path.

## Test lifecycle failures

Crash the worker after accepting, after an external side effect, and before recording success. Retry creation, send cancellation concurrently with completion, delete the target, revoke the caller, and poll after retention expires. Assert one durable outcome and an audit trail that connects request, operation, attempts, effects, and caller.

Retention needs two clocks: one for the operation metadata and another for large result artifacts. After an artifact expires, keep a terminal operation that explains the result is no longer available. Deleting the whole record too early makes a completed job indistinguishable from an invalid identifier.

The next step is to write the operation state diagram for one existing slow endpoint and implement creation plus read before adding progress or callbacks. Review by 2026-12-05 or when client retry behavior changes.

## Source references

- <https://google.aip.dev/151>
- <https://www.rfc-editor.org/rfc/rfc9110.html>
