Unified FHIR Async Operations Pattern

Nikolai Ryzhikov
November 27, 2025
7 min

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 through Bulk Data Access and the R5 Asynchronous Interaction Request pattern. They share the same lifecycle:

Highlighted Text
  • 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).
Highlighted Text

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).

Highlighted Text

R6 work aims to standardize async across all interactions. Each interaction now allows 202 Accepted, and Josh's draft spec 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.

Highlighted Text

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).

GET /fhir/[$operation] HTTP/1.1
Prefer: respond-async, async-mode=[redirect|bundle]   # async-mode is a proposed extension token
Highlighted Text

Servers SHOULD echo honored preferences via Preference-Applied..

Highlighted Text

Clients should assume unknown Prefer tokens are ignored and be prepared to fall back to the default bundle mode.

When the operation is completed:

Highlighted Text
  • 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 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:

Highlighted Text
  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:

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... }
Highlighted Text

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:

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

Bundle Mode

Redirect Mode

Cancellation Flow

Callback Mode

How did you like the article?

contact us

Get in touch with us today!

By submitting the form you agree to Privacy Policy and Cookie Policy.
Thank you!
We’ll be in touch soon.

In the meantime, you can:
Oops! Something went wrong while submitting the form.

Never miss a thing
Subscribe for more content!

Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.
By clicking “Subscribe” you agree to Health Samurai Privacy Policy and consent to Health Samurai using your contact data for newsletter purposes