# Unified FHIR Async Operations Pattern
Some operations are computationally intensive and can take minutes or hours (bulk export, search-parameter reindexing, full resource revalidation). Running them asynchronously prevents blocking the client and lets the server manage the load. In an async flow the client kicks off the job, the server runs it in the background, and the client either polls for status or receives a callback when it completes.
FHIR already provides [async support](https://hl7.org/fhir/async.html) through [Bulk Data Access](https://build.fhir.org/ig/HL7/bulk-data/en/export.html) and the [R5 Asynchronous Interaction Request pattern](https://hl7.org/fhir/async-bundle.html). They share the same lifecycle:
* Kick-off request -> `202 Accepted` with `Content-Location`.
* Status request (while running) -> `202 Accepted` with `Retry-After` and optionally `X-Progress`.
* Optional cancel -> `DELETE` to the status URL returns `202 Accepted`.
* Terminal status -> either `200 OK` (payload inline) or `303 See Other` (redirect to the payload).
Inline modes (Bundle, Bulk manifest) always return `200 OK` on completion; success vs. failure is indicated by the payload (e.g., a result Bundle vs. an `OperationOutcome`, or error links inside a manifest). Redirect mode surfaces the original synchronous status codes on the redirected request (e.g., `200/201` on success, `4XX/5XX` on failure).
The patterns mainly differ by the envelope for the final payload:
* Bulk Data Access returns a custom JSON manifest.
* Asynchronous Interaction Request returns a FHIR Bundle.
* Redirect mode returns whatever the synchronous interaction would have returned (Parameters, Bundle, Binary, etc.).
This breaks down when the payload cannot be expressed as a Bundle (e.g., streaming outputs or binary content).
R6 work aims to standardize async across all interactions. Each interaction now allows `202 Accepted`, and [Josh's draft spec](https://hackmd.io/@jmandel/async-pattern-simplified) proposes an additional redirect pattern: the terminal status response is `303 See Other` with a `Location` pointing to the result; that endpoint returns the actual payload and its status.
Ideally we can unify these patterns into a single parameterized one. Use `Prefer: respond-async` plus a custom `Prefer` token to request the mode. For backward compatibility the default is `bundle`; if `_outputFormat` is present, treat the request as Bulk and return a manifest (servers MAY reject a conflicting `async-mode` with `400 Bad Request`).
```http
GET /fhir/[$operation] HTTP/1.1
Prefer: respond-async, async-mode=[redirect|bundle] # async-mode is a proposed extension token
```
Servers SHOULD echo honored preferences via `Preference-Applied`.
Clients should assume unknown `Prefer` tokens are ignored and be prepared to fall back to the default bundle mode.
When the operation is completed:
* `async-mode=bundle`: server SHOULD respond with `200 OK` and a Bundle body; failures use an `OperationOutcome` payload.
* `_outputFormat` parameter present: server SHOULD respond with `200 OK` and a Bulk Manifest body; failures surface via manifest error links and/or an `OperationOutcome` entry.
* `async-mode=redirect`: server SHOULD respond with `303 See Other` and a `Location` URL pointing to the result of the operation; the redirected request returns the same status codes as the synchronous interaction.
Clients can infer completion and the mode from the status code (`200` or `303`); success vs. failure depends on the final payload or the redirected response status.
## Extensions
Here is a list of extra extensions which can be added to improve support for async operations:
### Long polling extension
Servers may use [long polling](https://en.wikipedia.org/wiki/Long_polling) for status requests to minimize latency. The server keeps the connection open until the operation is completed or a timeout occurs. On timeout the client retries, and the server controls the frequency of incoming status requests. Long polling reduces needless polling traffic while still delivering fast notification when the state changes (completion or cancellation).
### Callback extension
Purpose: allow the server to notify the client when a job finishes so the client can pause or stop polling.
Motivation: callbacks keep response latency low without aggressive polling. Clients can back off to slow, cheap polls (or none) while still getting near-real-time notification when the job finishes.
Flow:
1) Client includes `callback-url` with `Prefer: respond-async` (and optional `async-mode`).
2) When the job completes or is cancelled, the server sends one POST to that URL.
3) If the callback fails or never arrives, the client relies on polling.
Example kick-off with callback:
```http
POST /fhir/$operation HTTP/1.1
Prefer: respond-async, async-mode=redirect, callback-url=https://example.com/callback
Accept: application/fhir+json
Content-Type: application/fhir+json
{ ...normal operation body... }
```
Payload: a FHIR `Parameters` resource containing:
* `status` (`completed | failed | cancelled`)
* `resultUrl` (URL the client can fetch for the final payload)
* Optional `OperationOutcome` parameter when the job failed
Delivery: best-effort, no retries; callbacks are a signal, not the primary delivery channel (polling is). Callbacks SHOULD be idempotent so duplicates are harmless.
Security: servers SHOULD authenticate callbacks (e.g., OAuth2 bearer token, mTLS, or HMAC).
Client handling: respond with `2XX` on success; non-`2XX` just means the client will poll.
Example callback request:
```http
POST https://example.com/callback HTTP/1.1
Content-Type: application/fhir+json
Authorization: Bearer eyJhbGciOi...
{
"resourceType": "Parameters",
"parameter": [
{ "name": "status", "valueCode": "completed" },
{ "name": "resultUrl", "valueUrl": "https://fhir.example.com/whatever/path/1ab7162f-result" }
]
}
```
## Example Visual Flows
Here are some example visual flows for the different modes.
## Bulk Manifest Mode
```mermaid
sequenceDiagram
participant Client
participant Server
Note over Client,Server: Step 1: Kick-off (_outputFormat parameter)
Client->>Server: POST /$operation?_outputFormat=application/fhir+ndjson (Prefer: respond-async)
Server->>Client: 202 Accepted (Content-Location: /whatever/path/1ab7162f-status)
loop Polling Loop (Step 2)
Client->>Server: GET /whatever/path/1ab7162f-status
Server->>Client: 202 Accepted (Retry-After: 60)
end
Client->>Server: GET /whatever/path/1ab7162f-status (Final poll)
alt Job Succeeded
Server->>Client: 200 OK (Body: Bulk Manifest JSON with output links)
else Job Failed
Server->>Client: 200 OK (Body: Bulk Manifest JSON with error links and/or OperationOutcome)
end
```
## Bundle Mode
```mermaid
sequenceDiagram
participant Client
participant Server
Note over Client,Server: Step 1: Kick-off (async-mode=bundle)
Client->>Server: POST /$operation (Prefer: respond-async, async-mode=bundle)
Server->>Client: 202 Accepted (Content-Location: /whatever/path/1ab7162f-status)
loop Polling Loop (Step 2)
Client->>Server: GET /whatever/path/1ab7162f-status
Server->>Client: 202 Accepted (Retry-After: 60)
end
Client->>Server: GET /whatever/path/1ab7162f-status (Final poll)
alt Job Succeeded
Server->>Client: 200 OK (Body: Bundle)
else Job Failed
Server->>Client: 200 OK (Body: OperationOutcome or error Bundle)
end
```
## Redirect Mode
```mermaid
sequenceDiagram
participant Client
participant Server
Note over Client,Server: Step 1: Kick-off (async-mode=redirect)
Client->>Server: POST /$operation (Prefer: respond-async, async-mode=redirect)
Server->>Client: 202 Accepted (Content-Location: /whatever/path/1ab7162f-status)
loop Polling Loop (Step 2)
Client->>Server: GET /whatever/path/1ab7162f-status
Server->>Client: 202 Accepted (Retry-After: 60)
end
Client->>Server: GET /whatever/path/1ab7162f-status (Final poll)
Server->>Client: 303 See Other (Location: /whatever/path/1ab7162f-result)
Note over Client,Server: Fetch result from redirect endpoint.
alt Job Succeeded
Client->>Server: GET /whatever/path/1ab7162f-result
Server->>Client: 200 OK (Body: Parameters/Bundle/Binary/etc)
else Job Failed
Client->>Server: GET /whatever/path/1ab7162f-result
Server->>Client: 4XX/5XX (Body: OperationOutcome)
end
```
## Cancellation Flow
```mermaid
sequenceDiagram
participant Client
participant Server
Note over Client,Server: Step 1: Kick-off
Client->>Server: POST /$operation (Prefer: respond-async)
Server->>Client: 202 Accepted (Content-Location: /whatever/path/1ab7162f-status)
Note over Client,Server: Optional polling while job runs
Client->>Server: GET /whatever/path/1ab7162f-status
Server->>Client: 202 Accepted (Retry-After: 60)
Note over Client,Server: Step 2: Cancel
Client->>Server: DELETE /whatever/path/1ab7162f-status
Server->>Client: 202 Accepted (Cancellation acknowledged)
Note over Client,Server: Step 3: Confirm cancellation
Client->>Server: GET /whatever/path/1ab7162f-status
Server->>Client: 404 Not Found
```
## Callback Mode
```mermaid
sequenceDiagram
participant Client
participant Server
participant CallbackEndpoint
Note over Client,Server: Step 1: Kick-off (with callback-url)
Client->>Server: POST /$operation[_format=...] (Prefer: respond-async, async-mode=[redirect|bundle], callback-url=https://example.com/callback)
Server->>Client: 202 Accepted (Content-Location: /whatever/path/1ab7162f-status)
Note over Client,Server: Client can optionally poll for status
opt Polling (optional)
Client->>Server: GET /whatever/path/1ab7162f-status
Server->>Client: 202 Accepted (Retry-After: 60)
end
Note over Server,CallbackEndpoint: Server sends one callback when job completes
alt Job Succeeded
Server->>CallbackEndpoint: POST https://example.com/callback (Body: Status=completed, result-url=...)
else Job Failed
Server->>CallbackEndpoint: POST https://example.com/callback (Body: Status=failed, OperationOutcome=...)
end
Note over Client,CallbackEndpoint: Client receives notification and fetches result
Client->>Server: GET /whatever/path/1ab7162f-result
Server->>Client: 200/201/4XX/5XX (Final payload or OperationOutcome)
```