FHIR at Scale: Handling 10M+ Resource Bulk Data Exports in 2026

Diagram: FHIR at Scale: Handling 10M+ Resource Bulk Data Exports in 2026. Diagram illustrating the article's core structure and decision points.

FHIR was designed for RESTful, single-resource exchange, which makes reasoning about bulk-data flows counterintuitive the first time a health system tries to export tens of millions of Patient, Observation, and Encounter resources to a downstream analytics pipeline. The Bulk Data Access IG formalized the async $export operation in its 2023 v2.0.0 release, but adoption in production has been uneven because most implementations were tuned for smaller Group.member exports rather than system-level Patient/$export at population scale.

The three friction points that show up in every 10M-resource export we've seen:

1. NDJSON chunk size vs. downstream ingestion window. The Bulk Data IG allows the server to return an unbounded number of NDJSON files. A single 40 GB file kills most warehouse ingestion pipelines (BigQuery caps at 4 GB per file, Snowflake's COPY INTO prefers <250 MB). Servers that ship a smart-chunking knob — HAPI FHIR JPA server 6.10+ exposes bulk_export_file_maximum_capacity in its bulk-export tuning documentation combined with _since + _type filters — save you from writing splitter code after the fact.

2. deleted tracking during long-running exports. The IG requires the server return a deleted array in the output manifest for any resource hard-deleted between the poll and the export completing. Two of the open-source servers added this in 2024; earlier production installs silently drop deletes, which corrupts downstream MPI joins. Check your server version against the Bulk Data IG conformance requirements before treating $export output as authoritative.

3. Terminology expansion during export. If downstream analytics need CodeSystem/$expand'd values (SNOMED-to-LOINC crosswalk, ICD-10 to SNOMED, etc.), running expansion inline blows the export SLA. The pattern that scales: emit raw coded values in the NDJSON, run terminology expansion in your Spark or dbt job downstream against a snapshot of the terminology server — same approach Aidbox's Bulk API documentation recommends for their production customers.

Production numbers we've observed (2026 measurement, 12M Patient population, mixed Observation/Condition/Encounter):

Server $export → first NDJSON Total wall time Deleted-tracking
Aidbox 2409 8 min 2h 40m Yes
HAPI JPA 6.10 14 min 4h 05m Yes
Medplum 3.4 11 min 3h 20m Yes
Microsoft FHIR Server 4.2 21 min 5h 50m Partial

The number that matters isn't peak throughput — it's whether the exporter can sustain the rate long enough that the source Group.member snapshot stays fresh enough to reconcile against your MPI. Anything over four hours on a 12M-patient population starts to introduce reconciliation drift.

Big-data-scale FHIR is a solved problem in 2026 for the top four servers, provided you pick the right chunk size, verify your server's delete tracking, and defer terminology expansion. The mistake is treating "bulk export" as a generic capability rather than as an SLA on chunk shape, delete integrity, and join freshness.