FHIR delete vs expunge: what a DELETE really removes, and when data is gone for good

On most FHIR servers a DELETE is a soft delete: reads return 410 and search skips it, but history keeps the data. Expunge is the hard delete. Here is how they differ and who should hold each.

The short answer

In FHIR, DELETE [base]/Patient/123 removes the resource from normal reads and search: a read then returns 410 Gone, and searches no longer find it.1 The spec treats a delete as a special kind of history entry with no content, so on a server that keeps versions, the earlier versions can still be read through history.1 That’s a soft delete.

Expunge is the hard delete. On HAPI FHIR, the $expunge operation and DELETE ... ?_expunge=true physically remove data, and HAPI’s own source calls this unrecoverable without backups. Both are off by default.2 An AI agent may sometimes need DELETE. It should almost never be able to expunge.

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 the FHIR spec says a delete is

The R4 RESTful API spec defines the delete interaction in a few rules worth knowing exactly:

Situation What the spec says
Successful delete 200 with a body, 204 without, or 202 if the server won’t commit to the outcome1
Read after delete 410 Gone, or 404 if the server doesn’t track deletions1
Search after delete The resource no longer appears1
Delete of a missing or already-deleted resource No effect, still 200 or 2041
Server won’t delete this type 4051
Server won’t delete this one (say, it’s referenced) 4091
Conditional delete matching several resources Delete them all, or refuse with 412; the CapabilityStatement says which1
PUT to the deleted id Re-creates the resource1

Two of these trip people up.

First, a 200 or 204 doesn’t tell you a resource existed. Deleting something that was never there returns the same success. If an agent reports “I deleted the duplicate Patient”, the status code alone doesn’t confirm there was one. A read before the delete does.

Second, the spec leaves retention to the server. It says servers are free to completely delete a resource and its history when policy or business rules require it.1 So “soft delete” is the common behaviour, not a promise. Check your server’s documentation. Our post on FHIR versioning and history covers how version ids and _history work on the servers that keep them.

Expunge: the hard delete

Expunge isn’t part of the FHIR specification. It’s an operation HAPI FHIR (and products built on it, such as Smile CDR) add for when data must actually go: a record created by mistake with the wrong patient’s details, test data in production, or a legal request to erase.

Smile CDR’s documentation describes how it works on that stack:

  • A standard delete is logical; the old content stays readable through version-specific reads and history.4
  • $expunge runs at instance, type or system level.4
  • Its parameters choose what goes: expungeDeletedResources (resources already deleted), expungePreviousVersions (old versions of live resources) and expungeEverything, with limit capping each batch (default 1,000). Each needs its own permission in Smile CDR.4
  • The current version of a resource can only be expunged once the resource has been deleted.4

HAPI’s source code is blunt about the risk. Its $expunge setting is false by default, and the comment says the operation “is potentially dangerous since it allows a client to physically delete data in a way that can not be recovered (without resorting to backups)”, recommending appropriate security so non-administrators can’t use it.2

There’s a faster path too. DELETE Patient?_id=123&_expunge=true deletes and expunges in one request. HAPI’s setting for it is also off by default, and it skips most of the usual delete checks; the only check left is that no other resource references what’s being removed.2 Smile CDR runs it as a background batch job at the type level.4 An asynchronous job means the HTTP response comes back before the work is done, so “the request succeeded” and “the data is gone” are two separate facts.

Cascades and references

By default HAPI enforces referential integrity on delete: you can’t delete a Patient that an Encounter still points to.2 That’s a useful brake. An agent that tries to delete a patient with history gets a refusal rather than a hole in the record.

HAPI can relax it. With the CascadingDeleteInterceptor registered, a request with _cascade=delete (or the header X-Cascade: delete) deletes the target and the resources that reference it.3 Combine it with _expunge=true and HAPI submits an asynchronous job that expunges the target and every resource referencing it, transitively, up to the configured number of cascade rounds.3 If the rounds run out first, only the resources collected so far are expunged, and deeper ones stay.3

Read that once more from an agent’s point of view. One request, DELETE Patient?_id=P-1&_expunge=true&_cascade=delete, can permanently remove a patient and everything that points to them, in the background, with a partial result if the graph is deep. It’s the FHIR equivalent of ON DELETE CASCADE plus VACUUM FULL. Our post on cascades that deleted more rows shows how quickly that grows in SQL.

Delete and expunge side by side

FHIR DELETE HAPI $expunge / DELETE _expunge
Part of the FHIR spec Yes1 No, a HAPI operation
Data still readable afterwards Usually, through history14 No2
Can be undone Yes, re-create with PUT or restore from history1 Only from backups2
On by default in HAPI Delete is No2
Respects references 409 if the server enforces it1 _expunge checks references; cascade overrides23
Runs synchronously Yes _expunge and cascades run as batch jobs34

What to let an agent do

None of this is clinical advice; it’s about who can change a record system. A sensible split for AI tools writing to FHIR:

  1. Agents create and update; they don’t delete by default. Most agent work (drafting notes, filling forms, reconciling data) never needs DELETE. Our post on which EHR writes need approval sorts operations by risk.
  2. If an agent must delete, require approval and read first. Read the resource, show a person what will go, then delete. Because a delete of a missing resource also returns success1, the read is what confirms the target existed.
  3. Never give an agent expunge. Keep $expunge and _expunge disabled, or restricted to administrators, as HAPI’s own comments advise.2 Erasure should be a human decision with a ticket behind it.
  4. Block cascades from automated clients. If you register CascadingDeleteInterceptor, refuse _cascade and X-Cascade from agent credentials at your gateway.3
  5. Check, don’t trust, the response. After any delete, read the resource and expect 410. After an expunge job, check history is empty, and keep a backup you’ve tested restoring.
  6. Log who asked. Record which agent, on whose behalf, deleted what. A soft delete leaves the content in history; your log should say who caused it.

Questions people ask

Does a FHIR DELETE remove data from the database?

Usually not. The spec treats it as a history entry with no content, and earlier versions can remain readable, though servers may fully delete if policy requires.1

How do I permanently delete a FHIR resource on HAPI?

Enable expunge on the server, delete the resource, then run $expunge with expungeDeletedResources, or use DELETE ... ?_expunge=true if delete-expunge is enabled. Both are off by default.24

Can a deleted FHIR resource be restored?

After a plain delete, yes: a PUT to the same id re-creates it, and history keeps the old content on servers that version.1 After expunge, only from backups.2

For the full picture of agents writing to patient records, see AI agents writing to patient records. 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

A FHIR DELETE usually leaves history readable; HAPI's $expunge and DELETE _expunge erase for good, are off by default, and shouldn't be given to an agent.

Read nextFHIR transaction vs batch: what the spec requires, and what it leaves open Try it: install in 7 s

Related on this site

More posts

Sources

  1. 1HL7 FHIR R4: RESTful API (delete) · hl7.org · checked 27 October 2026
  2. 2HAPI FHIR source: JpaStorageSettings (expunge and delete settings) · github.com · checked 27 October 2026
  3. 3HAPI FHIR docs: JPA server configuration (cascading deletes, delete with expunge) · hapifhir.io · checked 27 October 2026
  4. 4Smile CDR docs: Deleting data · smilecdr.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