FHIR If-Match version conflicts: why you get 409 when the spec says 412

FHIR R4 says a stale If-Match gets 412 Precondition Failed. In our October 2026 study, all four server setups answered 409 instead. The write was refused either way. Here is how to handle both.

The short answer

If your FHIR update sends If-Match with an old version and the server answers 409 Conflict instead of 412 Precondition Failed, nothing went wrong with your request: the server refused the stale write, which is what matters. FHIR R4 names 412 for a version mismatch, and also says 409 can be returned when the server detects the update can’t be done.1 In our October 2026 study, all four FHIR server setups we tested answered 409.3 Handle both codes the same way: re-read the resource, compare, then decide.

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

What R4 says

A version-aware update sends the version the client last read in an If-Match header, as a weak ETag such as W/"3". If that version doesn’t match the current one, the server returns 412 Precondition Failed and doesn’t update the resource.1 The same section notes that a 409 Conflict can be returned when the server detects that the update cannot be done, and the status list for updates gives “409/412” for version conflict management.1

So both codes appear in the spec. The specific rule for a mismatched If-Match names 412; the general list allows either. A client that only checks for 412 will mishandle real servers.

The version itself lives in Meta.versionId. It changes each time the resource’s content changes and is usually managed by the server.2 You can read any earlier version with vread at [base]/[type]/[id]/_history/[vid].1

What four servers returned

On 26 October 2026 we sent synthetic FHIR R4 writes to four server setups and read every one back: HAPI FHIR JPA Server 8.12.0 run locally twice (starter defaults, and with request validation on), the HAPI public test server (build 8.13.10-SNAPSHOT) and the Firely Server 6.11.0 public sandbox.3 The full method and data are on the FHIR write integrity study.

Setup Stale If-Match reply Write applied?
HAPI 8.12.0, starter defaults 409 No
HAPI 8.12.0, request validation on 409 No
HAPI public test server 409 No
Firely Server 6.11.0 sandbox 409 No

All four refused the stale If-Match with 409, where R4 names 412.3 The important part held everywhere: the stale write was refused. This is a difference in status code, not a lost update.

A worked example

Two writers touch the same Observation. The agent reads it at version 3, a nurse corrects it (now version 4), and the agent then tries to save its own edit based on version 3.

Code A request one program sends to another over the web.

GET /fhir/Observation/obs-17
→ 200 OK, ETag: W/"3"

# a person updates the resource: it is now version 4

PUT /fhir/Observation/obs-17
If-Match: W/"3"
Content-Type: application/fhir+json
{ "resourceType": "Observation", "id": "obs-17", ... }
→ 409 Conflict   (R4 names 412 here; either means "refused")

Without If-Match, the agent’s PUT would have replaced the nurse’s correction silently. That is the lost-update problem the header exists to stop. With it, the server refused the write, and the agent now has to decide what to do.

What an agent should do on a conflict

A conflict means someone else changed the record after the agent read it. The safe responses, in order:

  1. Stop. Treat 409 and 412 on a conditional update as the same signal. Don’t retry the same body; it will fail again, and if the retry drops If-Match it will overwrite the other change.
  2. Re-read the current version and the version the agent based its edit on (vread of the old version).1
  3. Compare the two. If the other change touched different fields and your rules allow it, rebuild the edit on the new version. If it touched the same fields, hand it to a person.
  4. Write again with the new version in If-Match, then read back and check the stored resource matches what you meant.
  5. Record the conflict, what was compared and who decided.

There’s a trap on the success path too. An update that changes nothing gets 200 and creates no new version on all four setups we tested; that’s R4 behaviour.3 So “200 and the version didn’t move” can mean “nothing to change”, not “saved”. Read back to tell which.

Reply to a conditional update Means Agent action
200 or 201, version went up Stored a new version Read back and compare fields
200, version unchanged No change was made Check you sent what you meant
409 or 412 Version conflict, refused Re-read, compare, maybe ask a person
400 or 422 The request itself was refused Fix the request; don’t retry as is

A checklist

  • Always send If-Match on updates an agent makes, with the version it actually read.1
  • Accept 409 and 412 as the same outcome.1
  • Never retry a conflicted write without If-Match.
  • Keep the version the agent read next to the version it wrote, so a reviewer can see which edit it built on.
  • Read back after every successful write; the reply alone doesn’t show what was stored.
  • Route same-field conflicts to a person.
  • Test your client against the servers you actually run, and name versions when you write it down. Status codes differ by server and build.

Questions people ask

Is a 409 instead of 412 a bug in the server?

We don’t call it one. R4’s rule for a mismatched If-Match names 412, but the same section allows 409 when an update can’t be done.1 What matters for safety is that the stale write was refused, and on all four setups it was.3

Do I need If-Match if only one agent writes?

Yes, if people or other systems can also edit the record. A nurse, an interface engine or a second agent can change it between your read and your write. Without If-Match, the last write wins silently.

Does a 200 mean my update was stored?

It means the server accepted it. An update that changes nothing also gets 200 and keeps the same version.3 Read the resource back and compare.

To see a read-back catch a write that said success, run the 15-minute pilot on synthetic data with npx deedglass health pilot4; the hospital page explains what it shows, and the record spec shows how a conflict and its resolution can be written down so anyone can check it later. To try approvals and read-backs on a real database in your browser, open the playground; to grade what an agent can reach on your own machine, run npx deedglass scan (the grade, explained).

Written with AI assistance and checked against our claims register.

The takeaway

All four FHIR setups we tested refused a stale If-Match with 409, where R4 names 412; treat both as 're-read, then decide', never as 'retry blindly'.

Read nextValidating FHIR writes in HAPI: request validation, repository validation, agents Try it: install in 7 s

Related on this site

More posts

Sources

  1. 1HL7 FHIR R4: RESTful API, Managing Resource Contention · hl7.org · checked 27 October 2026
  2. 2HL7 FHIR R4: Resource, Meta.versionId · hl7.org · checked 27 October 2026
  3. 3Deedglass research: FHIR write integrity 2026 · deedglass.com · checked 27 October 2026
  4. 4Deedglass for hospitals: the 15-minute pilot · deedglass.com · checked 27 October 2026

Published 27 October 2026, sources last checked 27 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