---
# System prepended metadata

title: Generalized Async Pattern

---

# 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)
```
