Two Bundles walk into a server. One is a transaction, one is a batch. They look almost identical on the wire — same POST, same URL, same entry list. The only difference is the Bundle.type field, and that field decides whether the server treats the whole thing as one atomic operation or as a collection of independent ones. Getting that decision wrong is a small change with a large blast radius.
The site's FHIR RESTful API cheatsheet has both forms laid out. For the broader REST framing, FHIR background reading collects related pieces.
Transaction: All or Nothing
A transaction Bundle succeeds as one unit or fails as one unit. If any entry fails validation, terminology check, referential integrity, or business rule, none of the entries are committed and the server returns an OperationOutcome describing what broke.
The atomic guarantee is the whole point. Use transaction when the entries have to be visible together — a Patient plus their initial Observation plus the Condition that motivated the encounter should be visible atomically, or not at all.
Batch: Each Entry Stands Alone
A batch Bundle runs each entry independently. Some succeed, some fail, and the response Bundle describes the outcome per entry. A Patient create can succeed while an Observation create fails, and the Patient stays in the server.
Use batch when the entries are semantically independent — bulk import of Patients where per-record failures are recoverable, or a mixed-entry client that wants to attempt several actions in one round trip without committing to atomicity.
The Field That Decides Everything
The Bundle.type field carries "transaction" or "batch" as a code. The value is not free-form. A client that sends "transaction" when it wants batch semantics is asking for a rollback that will surprise everyone. A client that sends "batch" when it wants atomic semantics is inviting partial-commit bugs. Test that field explicitly.
Referential Integrity Only Holds in Transaction
The subtle win is references between entries. In a transaction, a Patient can be created with a temporary reference urn:uuid:..., and an Observation in the same Bundle can reference that Patient via the same urn. The server rewrites the references to real ids atomically. In a batch, that trick does not work — each entry is independent, and the Observation entry gets processed before the Patient id exists.
If your Bundle uses urn: references between entries, it has to be a transaction.
Status Code Semantics
Transactions return 200 OK on full success, or an error status matching the underlying failure with an OperationOutcome. Batches always return 200 OK — the per-entry outcomes live inside the response Bundle. A client that reads the outer status and assumes success on a batch is going to miss failed entries silently. For the status side, HTTP status codes FHIR reuses vs redefines is the deep dive.
Server Support Is Uneven
Not every FHIR server implements transaction Bundles. Some support batch only. The CapabilityStatement declares which. Read it once at startup and cache the answer. A client that hard-codes transaction support may work against one server and fail unexpectedly against another.
The Practical Rule
Default to batch for anything import-shaped. Reach for transaction when the entries are semantically coupled — same patient, same encounter, same clinical event — and the atomic guarantee is load-bearing. Do not default to transaction "just in case" — the server-side work is heavier and the failure modes are less graceful.
For the survey of REST verbs where FHIR differs from vanilla, the FHIR REST verbs that actually differ from vanilla REST is the entry, and designing a FHIR client that survives the tricky verbs is where the shape lands.
Sources
- HL7 canonical FHIR Bundle resource specification defining - HL7 canonical FHIR Bundle resource specification defining transaction and batch