A GDPR 'delete' can pass every check while 5 of 6 stores keep the data
'We deleted the row' verifies a deletion executed; GDPR Article 17 asks that no recoverable copy survives. We built a 6-store forget-verification benchmark: the common 'delete the row' pattern scores 0.17 (five stores still leak) vs 1.00 for a correct hard-delete, plus a signed proof-of-erasure.
A right-to-erasure request has a precise meaning, and most stacks quietly answer a weaker one. "We deleted the row and logged it" verifies that a deletion executed. GDPR Article 17 asks something else: that no recoverable copy survives. Those are different claims, and the distance between them is where the risk lives.
We built a small benchmark to measure that distance, and the number is uncomfortable.
A delete can pass every check while 5 of 6 stores keep the data
A fact in a real agent/RAG stack does not live in one place. It lives in the primary log, the vector index (as an embedding), an embedding/response cache, and — depending on your infra — a Qdrant collection, a pgvector table, and an S3 snapshot. Delete the primary row and log it, the common pattern, and here is what a forget-verification audit finds across that six-store fan-out:
| deletion strategy | forget-verification score | stores that still leak |
|---|---|---|
| delete the row + log it (the common bug) | 0.17 | vector-index, embed-cache, qdrant, pgvector, s3-snapshots |
| hard-delete + reindex everywhere | 1.00 | none |
Score is the fraction of stores from which the value is no longer recoverable after the deletion. The "delete" that returned 200 left the data recoverable in five of six places.
Why a surviving embedding is not inert
This part is not new, but it is worth restating: a retained embedding reconstructs its source. Morris et al. showed roughly 92% exact reconstruction of short inputs from their embeddings ("Text Embeddings Reveal (Almost) As Much As Text", EMNLP 2023), and Ghost Vectors (arXiv 2606.18497) shows soft-deleted vectors stay reconstructible inside HNSW indexes. So "we deleted the row" can be true at the same time the content is one nearest-neighbour query away.
"The API returned 200" and "it's gone" are separated by a background process
The vector engines make it worse by default. Qdrant marks deleted points in a bitmask and only physically drops them once a segment crosses the optimizer's deleted_threshold (0.2, with a 1000-vector minimum) — scattered deletes can sit under that line indefinitely. pgvector inherits Postgres MVCC: the tuple is dead but on disk until VACUUM runs, and the HNSW graph is repaired only then. And on a versioned S3 bucket a "delete" is just a delete marker written over a live version, one list-object-versions call away. So the deletion the app thinks it made and the physical state on disk are separated by a compaction, a vacuum, or a garbage-collect that may never trigger.
Proof, not a log
The fix is not a better deletion log. It is to treat a delete as a claim that gets adversarially tested, and to emit a signed artifact when it passes. inspeximus's ErasureAuditor registers each store a subject's data can live in, re-attempts recovery from every one after your app's deletion (a verbatim scan for text and caches, a nearest-neighbour reconstruction for surviving vectors, a soft-delete-state check for Qdrant/pgvector/S3), and compliance_receipt() packages the outcome as a signed, verifiable proof-of-erasure — the artifact a data protection officer can actually hand a regulator, tamper-evident under their own key.
```python from inspeximus.erasure_auditor import ErasureAuditor, verify_compliance_receipt, ed25519_verify
receipt = auditor.compliance_receipt("user:alice", ["12.69"], sign=my_signer, pubkey=my_pk, request_id="dsar-2026-07", basis="GDPR Art.17")
verify_compliance_receipt(receipt, ed25519_verify) # (True, "ok") ```
The honest scope
This audits the stores you register — not backups you don't, and it can't prove physical destruction; the vector check is a lower bound on the leak, since full inversion is stronger than a nearest-neighbour reconstruction. When a store leaks, the fix is a hard-delete plus reindex, or crypto-shredding the key rather than the row. But it changes what you can say from "we ran the deletion" to "no registered store still has it, and here is the signed proof" — which is the claim Article 17 was always about.
It is open (MIT), zero-dependency, and the benchmark is runnable: pip install inspeximus.
FAQ
Does "we deleted the row" satisfy GDPR Article 17? Not on its own. Article 17 is about the data no longer being recoverable, not about a deletion having executed. A retained embedding, a soft-deleted vector, or an S3 delete-marker can leave the content recoverable while the deletion log says done.
Where does a "delete" usually survive? The vector index (the embedding reconstructs the text), an embedding/response cache, and soft-delete residue in the engine itself — Qdrant points under the optimizer threshold, pgvector dead tuples before VACUUM, an S3 delete-marker over a live object version.
What is a forget-verification score? The fraction of the stores a subject's data can live in from which the value is no longer recoverable after your deletion. A correct hard-delete scores 1.00; the common "delete the row" pattern scored 0.17 in our six-store benchmark.