The FHIR Bulk Data spec is careful about a lot of things. It defines the kickoff request, the status-polling loop, the manifest format, the delivery URLs, and the cleanup step. The one thing it deliberately does not define is a duration estimate. That omission is not an accident; it is a design decision that reflects how much variance real bulk exports carry.

Naming the omission is the discipline. For related walkthroughs, more on healthcare data exchange collects the surrounding material.

The Spec Uses Retry-After as a Hint

The polling response carries a Retry-After header that says when to check again. The header is a polling cadence hint, not a duration estimate. A server that returns Retry-After: 60 is saying "come back in a minute", not "this export will finish in a minute."

Clients that treat the header as an ETA end up spinning in tight loops when the export is short and going idle when the export is long. Neither pattern is useful. A pass through the site's export duration estimator fills the gap by projecting duration from benchmarked engine behavior.

Why Servers Do Not Publish an ETA

Server implementers face the same variance the clients see. The engine does not know how long the export will take before it starts because the duration depends on the scope, the archive size, the concurrency budget, the persistence tier, and any traffic contention that arrives during the export.

Publishing a wrong ETA is worse than not publishing one. The spec's response is to publish neither. That is honest and inconvenient in equal measure.

The Client Is the Right Place to Predict

The prediction has to live client-side because the client is the one that owns the scope and can decide whether the export is on schedule. For the full prediction method, predicting how long a bulk data $export will actually take is the accompanying reference.

Client-side prediction combines the resource-count estimate, the engine's benchmarked serialize rate, and the delivery method's throughput signature. The result is a range that tightens as the poll returns manifest-level progress.

Progress Reporting Is Sparse

The spec allows the polling response to carry an X-Progress header with a free-form progress hint. The header is optional and the format is not standardized. Some servers report percentages; some report resource counts; some report nothing at all.

Clients that rely on the header find it works for one server and fails silently for another. Sizing plans should not depend on the header being present. For the specific driver framing, the four things that dominate $export duration covers what the client should measure instead.

Manifest-Level Progress Is Where It Lives

The most reliable client-side progress signal comes from the manifest. The manifest lists the files the export will produce. As files appear, the client can count them and compute a percentage. The percentage is accurate because it measures what the server actually delivered.

Manifest-based progress is what monitoring $export progress from the client side walks through in detail. Every bulk-data client that runs at scale ends up using some version of it.

Building a Realistic Estimate

The realistic ETA the spec cannot give you is a client-side function of scope, engine benchmarks, delivery method, and observed manifest progress. Combining those inputs produces a range that survives the export duration. Servers that publish helpful benchmarks make the prediction easier; clients that measure carefully make the prediction accurate.

The spec left the ETA out on purpose. Every deployment that runs bulk exports at scale fills the gap deliberately.

Retro-crt-grain diagram of the bulk-export lifecycle from kickoff to poll to files to cleanup with a warm burnt-orange scanline overlay and retry arrows highlighted

Sources