FHIR transaction vs batch: what the spec requires, and what it leaves open

Both are Bundles you POST to the server base. One is atomic and can link its entries together; the other isn't and can't. Mixing them up is how half a write ends up stored. The R4 rules, with an example.

The short answer

In FHIR R4, a transaction Bundle is all-or-nothing: “all interactions or operations either succeed or fail together”.1 Its entries can refer to each other through temporary ids, and the server rewrites those references to the real ids it assigns. A batch Bundle is a set of independent requests: entries must not depend on each other, and the server answers 200 if it processed the batch, even when some entries failed.1

So use a transaction when the entries only make sense together, such as a Patient and the Encounter that points at it. Use a batch for unrelated requests you want in one round trip. Either way, read every entry in the response, and check that your server really runs transactions atomically: some servers need a setting for that.

A FHIR server can accept a write and quietly drop a field. Deedglass reads the resource back and names what differs. Made-up resource.

Side by side

Batch Transaction
Bundle type batch transaction
Atomic? No Yes, all or nothing1
Entries may depend on each other? No; references to resources created in the same batch are non-conformant1 Yes, through fullUrl
HTTP status when some entries fail 2001 400 or 500 type, nothing applied1
Response on success batch-response Bundle transaction-response Bundle
Response on failure batch-response with failing entries One OperationOutcome1
Same resource twice? Not allowed by the independence rule Not allowed: once per identity1

The 200 for a batch is the row that causes trouble. A client that checks only the HTTP status will believe every entry succeeded.

How a transaction is processed

The spec says the outcome of a transaction SHALL NOT depend on the order of its entries, and fixes the order the server works in:1

  1. DELETE interactions
  2. POST interactions
  3. PUT or PATCH interactions
  4. GET or HEAD interactions
  5. Resolve conditional references

Two consequences. First, a read inside a transaction sees the results of the writes in it, because reads come after writes. Second, you can’t delete a resource and recreate it in the same transaction, or update it twice: a resource can appear only once by identity, and overlapping identities fail the transaction.1

Linking entries with urn:uuid

The reason to use a transaction is usually that one new resource must point at another new resource. You don’t know the server’s ids yet, so you use temporary ones:

Code Data in JSON, the format software uses to pass records and settings.

{
  "resourceType": "Bundle",
  "type": "transaction",
  "entry": [
    {
      "fullUrl": "urn:uuid:7c1e4f9a-2b6d-4d1e-9f0a-1a2b3c4d5e6f",
      "resource": { "resourceType": "Patient", "name": [{ "family": "Example" }] },
      "request": { "method": "POST", "url": "Patient" }
    },
    {
      "resource": {
        "resourceType": "Encounter",
        "status": "finished",
        "class": { "code": "AMB" },
        "subject": { "reference": "urn:uuid:7c1e4f9a-2b6d-4d1e-9f0a-1a2b3c4d5e6f" }
      },
      "request": { "method": "POST", "url": "Encounter" }
    }
  ]
}

For a POST, the server treats the fullUrl as the resource’s id on the client side, ignores it, assigns a new id, and SHALL replace all matching links in the Bundle.1 The stored Encounter’s subject then points at Patient/<new id>.

Send the same thing as a batch and the Encounter’s reference to urn:uuid:… is non-conformant, because batch entries may not refer to resources created in the same batch.1 What a server does with it varies; the spec doesn’t promise a helpful error.

Conditional references

Inside a transaction, a reference can be a search instead of an id, such as Patient?identifier=http://hospital.example/mrn|12345. The server runs the search. If exactly one resource matches, it replaces the search with a reference to that resource. If none or several match, the transaction fails.1

This is the safe way to attach new data to an existing patient without looking up the id first, and the failure on several matches is a feature: it stops a write from being attached to the wrong person when identifiers aren’t unique. Conditional references aren’t available in batches.

A related tool, conditional create with If-None-Exist, behaves differently: one match returns 200 and creates nothing, and more than one returns 412.1 Inside a transaction, plan for both outcomes.

Choosing between them

A simple rule covers most cases. If any entry refers to another new entry, or if a partial result would leave the record in a state nobody intended, use a transaction. A new patient with their first encounter, or a medication request with the dispense that depends on it, belong together.

If the entries are unrelated and a partial result is fine, a batch saves round trips: loading a set of independent observations, or reading several resources at once. Expect some entries to fail and plan to retry only those.

When an AI agent builds the Bundle, prefer transactions for anything it writes. A model is likely to assume that “one request” means “one outcome”, which is only true for a transaction.

Reading the response

On success, the response Bundle has one entry per request entry, in the same order.1 Each entry’s response carries a status, and for creates and updates a location with the new id and version.

A checklist for clients, and for agents’ FHIR tools in particular:

  1. For batches, check every entry’s response.status. Anything other than 2xx failed.1
  2. For transactions, treat any non-200 as “nothing happened” and read the OperationOutcome.
  3. Record the location of each created resource, including the version. You need it to read the write back.
  4. Don’t count requests as writes. A conditional create can answer 200 with nothing new stored.1
  5. Send If-Match on updates inside the Bundle where you know the version, so a concurrent change makes the entry fail rather than overwrite.1

When a transaction is quietly a batch

The spec describes what a transaction must do. Not every server does it by default. Medplum, for example, documents that it processes a transaction Bundle as a batch unless the project turns on its transaction-bundles feature. In our benchmark on Medplum 5.2.6 without that feature, a transaction with one invalid entry was answered 200, and two of its three Observations were stored.2 To the client, it looked like a successful transaction; on the server, it was a partly applied batch.

That is one of 17 scenarios in our FHIR write benchmark, where a 200 or 201 didn’t mean the server stored what the client meant. The lesson generalises: check your server’s settings, then test with a deliberately invalid entry and confirm nothing was stored.

For the versioning that makes read-backs possible, see FHIR versioning and history; for the idea of checking a write after it lands, the read-back check. Nothing here is clinical; it’s about whether the server stored what was sent. To try checks like this on a real database in your browser, open the playground; to grade what an agent can reach on your machine, run npx deedglass scan (the grade, explained).

Written with AI assistance and checked against our claims register.

The takeaway

A FHIR transaction succeeds or fails whole and links urn:uuid entries; a batch answers 200 even when entries fail. Check every entry.

Read nextFHIR versioning: versionId, ETag, vread and _history in one page Try it in your browser

Related on this site

More posts

Sources

  1. 1HL7 FHIR R4: RESTful API (Batch/Transaction) · hl7.org · checked 26 October 2026
  2. 2Medplum docs: batch requests · medplum.com · checked 26 October 2026
  3. 3deedglass/deedglass: FHIR write-integrity benchmark results · github.com · checked 26 October 2026

Published 26 October 2026, sources last checked 26 October 2026. If something here is out of date, write to hello@deedglass.com.

Try it on your own setup

npx deedglass scan

Grades only your own setup. Nothing is uploaded.

Pricing