Healthcare · Data platforms

FHIR Bulk Data export: building population data pipelines that survive production

Bulk FHIR is the standard way to get population-level data out of a certified EHR. The specification is short. The production pipeline around it is not, and most of the work is in the parts the spec leaves to you.

Field noteSumanta Mukhopadhyay8 min read6 sources
#healthcare#fhir#data#platform
FIELD NOTE · HEALTHCARE · DATA PLATFORMS20 Oct 2026 · 08:15 GST

Most healthcare analytics and AI projects start with the same question: how do we get the data out? For US EHRs, the standard answer is FHIR Bulk Data. ONC certification for the standardised API criterion points to SMART Backend Services for system-to-system authorisation, and multi-patient access is built on the Bulk Data Access specification4. So if you are building a population health platform, a research warehouse or a clinical AI product, you will probably be writing a bulk FHIR client.

The specification itself is compact. What decides whether the pipeline works in production is everything around it: scheduling, retries, incremental loads, validation and what happens downstream.

How the flow works

Bulk export uses FHIR's asynchronous request pattern, which exists because ordinary synchronous REST calls break down for large result sets3. In outline:

  1. Authorise. The client authenticates as a backend service, typically by signing a JWT with its registered key and exchanging it for an access token4.
  2. Kick off. The client calls $export at the patient, group or system level with Prefer: respond-async. The server replies 202 Accepted with a Content-Location status URL3.
  3. Poll. The client polls the status URL. Servers may send Retry-After, and the R4 async page tells clients to back off accordingly3. Some vendors return 429 if you poll too early6.
  4. Collect the manifest. On completion the server returns a manifest with a transactionTime, whether downloads need a token, an output list of NDJSON files per resource type and an error list of OperationOutcome files5.
  5. Download and clean up. The client fetches each file, then can delete the job by sending DELETE to the status URL5.

The current published version of the IG is STU2 (2.0.0), on FHIR R41. Check which version each EHR vendor supports. Behaviour still differs between implementations, so I read the vendor's own documentation alongside the IG.

Where pipelines break

FailureWhat it looks likeThe design response
Long-running jobsExports take hours, tokens expire mid-downloadTreat each export as a durable job with state; refresh tokens per file, not per job
Partial successA manifest with outputs and errors togetherLoad what succeeded, record what failed, alert on error files rather than ignoring them
Polling too eagerlyThrottling and 429 responsesHonour Retry-After; use exponential backoff when it is absent
Duplicate loadsThe same resource arrives in two exportsUpsert by resource ID and version; make every load idempotent
Silent schema driftNew extensions or profiles appear after an EHR upgradeKeep raw NDJSON, validate against expected profiles, quarantine what doesn't parse

The single most useful rule: land the raw NDJSON untouched in object storage before any transformation. When something goes wrong downstream, and it will, you can replay from the files instead of asking a hospital to run another export.

Incremental loads and the _since trap

Full exports are expensive for the source system and for you. The standard approach is to pass the previous manifest's transactionTime as _since on the next run, so you only receive resources updated after it.

There is a subtlety for Group exports. The 2.0.0 changes widened the Group-level _since definition: for patients added to the group after the supplied time, servers may return resources with an earlier last-updated date12. That is the right behaviour, because a new cohort member's history is new to you, but a loader that assumes everything is newer than the watermark will mishandle it. Store the manifest's transactionTime as the watermark, never your own clock, and compare versions per resource rather than trusting the export window.

Use _type to request only the resource types you need1. Exporting everything "just in case" multiplies cost and widens the amount of ePHI you hold, which matters for the controls in the proposed HIPAA Security Rule update.

The reference architecture I use

  1. An export orchestrator that runs each source on a schedule, persists job state, handles polling and retries, and records every manifest.
  2. A raw landing zone of encrypted, immutable NDJSON files, partitioned by source, resource type and export time.
  3. A validated FHIR layer that parses, validates and deduplicates resources, with a quarantine for failures and per-source data quality metrics.
  4. Analytical models on top: OMOP for research and AI, as in FHIR to OMOP, checked with the tools in OMOP data quality checks.
Diagram of the OMOP Common Data Model showing clinical data, vocabulary, health system and derived element tables
The OMOP Common Data Model, a common analytical destination for bulk FHIR data. Source: OHDSI, The Book of OHDSI: The Common Data Model · CC0 1.0

Each layer can be rebuilt from the one below it. That property matters more than any particular tool choice, because mapping logic will change many times over a platform's life.

Questions to ask each EHR source

  • Which Bulk Data IG version and which export levels (patient, group, system) are supported?
  • How are groups defined and maintained, and who can change membership?
  • What are the rate limits, typical export durations and maintenance windows?
  • Are download URLs protected by tokens, and how long do they stay valid?
  • Which resource types and profiles are populated in practice, not just supported on paper?

The answers shape the orchestrator more than the spec does. Write them down per source; they are the first thing a new engineer needs.

How I can help

I design healthcare data platforms from EHR extraction through to research-ready OMOP and clinical AI, with security and data quality built in. See healthcare data architecture, or analyse your challenge and bring the result to a call.

References
  1. 1
  2. 2
  3. 3
  4. 4
  5. 5
    FHIR Bulk Data ExportOystehr documentation
  6. 6
    Bulk Export (FHIR Bulk Access)Greenway Health developer documentation
FAQ

Common questions

What is FHIR Bulk Data export?

An HL7 implementation guide that defines a $export operation for exporting data for all patients, a group of patients or a whole system. It uses FHIR's asynchronous request pattern and returns files in newline-delimited JSON (NDJSON).

How does the asynchronous flow work?

The client sends a kick-off request with a Prefer: respond-async header. The server returns 202 Accepted with a Content-Location status URL. The client polls that URL, respecting any Retry-After header, until it receives a manifest listing the output files to download.

How do I load only changes since the last export?

Use the _since parameter with the transactionTime from the previous export's manifest. Note that for Group exports, the IG allows servers to return older resources for patients added to the group after that date, so your loader must handle them.

Keep reading

Related

Open to advisory, fractional CTO & select builds

Let's talk about what you're building.

The fastest route is a free 30-minute call. Prefer to write? Send a message and I'll reply personally within two business days.

Start a conversation

How would you like to reach me?

Book a 30-min callFastest. Pick any open slot on my calendar.→ Write in GmailOpens a pre-filled draft in your browser.→ Use my email appOutlook, Apple Mail or your default client.→

I reply personally within two business days. US & UAE time zones.