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.
Deedglass
The server said “saved”. Deedglass looked.
Deedglass reads it back
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:
- 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-Matchit will overwrite the other change. - Re-read the current version and the version the agent based its edit on (
vreadof the old version).1 - 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.
- Write again with the new version in
If-Match, then read back and check the stored resource matches what you meant. - 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-Matchon 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.