Deletion
How to test that deleting a user actually deletes them
Erasure is easy to promise and hard to prove. A step-by-step method to test deletion across databases, replicas, warehouses, logs, backups and third-party tools.
Anoop Thiparala · Founder, Farwatch Systems
9 min read
Most products have a "delete my account" button. Behind it, there is usually a job that removes a row from the users table, sometimes with a few related tables. The person's data is still in the read replica's lag window, the search index, last night's warehouse snapshot, the CSV export someone put in a bucket, the product analytics tool, the support desk, the error tracker and every backup.
India's DPDP Act turns that gap into a legal problem, and retention and deletion are already standard questions in customer and investor due diligence. This article explains what the law asks for, then gives a test method you can run with your own team: create a test user, let their data spread, delete them through the real flow, and check every place they could still be.
What the law asks for#
The DPDP Act, 2023 and the DPDP Rules, 2025 require erasure in three situations:
- When consent is withdrawn, or "as soon as it is reasonable to assume that the specified purpose is no longer being served, whichever is earlier" (section 8(7)(a)).
- When the person asks (section 12(3)). You must erase "unless retention of the same is necessary for the specified purpose or for compliance with any law for the time being in force".
- After long inactivity, for very large platforms. The DPDP Rules, 2025 set a three-year inactivity limit, with a warning 48 hours before. It applies to e-commerce entities and social media intermediaries with at least two crore (20 million) registered users in India, and to online gaming intermediaries with at least fifty lakh (5 million) (Rule 8 and the Third Schedule).
The clause that makes this an engineering problem is section 8(7)(b). You must "cause its Data Processor to erase any personal data that was made available by the Data Fiduciary for processing to such Data Processor". Deleting from your own database is half the job. Your analytics, CRM, email, support and error-tracking vendors must delete too.
Withdrawal of consent has its own duty: you must "cease and cause its Data Processors to cease processing" within a reasonable time (section 6(6)). Test that separately, because stopping the flow of new data is a different mechanism from deleting old data.
What you may, or must, keep
Erasure has exceptions, and they shape the design.
- Other laws. The Act's own example is a bank that must keep identity records for ten years after an account closes (illustration to section 8(7)). Tax, medical-records and sector rules can require similar retention.
- The one-year rule. Rule 8(3) says you must keep "such personal data, associated traffic data and other logs of the processing for a minimum period of one year from the date of such processing", for the State purposes listed in the Seventh Schedule, and then erase them. The Rule's illustration is an e-book platform that must keep order details, personal data and processing logs for a year "even if X deletes her account". Lawyers are still debating how broadly this applies.
- Security logs. Rule 6(1)(e) asks you to keep logs and personal data for a year, so you can detect and investigate unauthorised access.
So a good deletion design has two stages. First, remove the person from every operational system and vendor. Then put whatever you are required to keep into a restricted store, with a recorded reason and an expiry date, and purge it automatically when the date passes. Decide with your lawyer what goes into that store. The engineering job is to make sure nothing else survives.
What "deleted" should mean in each store#
The Act and Rules don't define erasure, anonymisation or pseudonymisation, so the table below is our engineering view. Agree the standard with your lawyer, then pick one outcome for each place the data lives and write it down:
| Outcome | What it means | Our engineering view |
|---|---|---|
| Hard delete | The record is removed, along with derived copies such as indexes and caches | Erased |
| Anonymise | Identifying fields are removed or replaced, and nothing can link the record back to the person | Erased, if it really can't be reversed |
| Retain under an exception | Kept for a documented legal reason, access restricted, with an expiry date | Kept, for that reason only |
| Soft delete | A deleted_at flag; the data is still there | Not erased |
| Pseudonymise | Fields replaced with tokens that a lookup table can reverse | Not erased while the lookup exists |
Soft deletes are the most common gap. They are fine as a short undo window, but a job must turn them into hard deletes or anonymisation on a schedule.
The deletion test#
Step 1: List every place a user can exist
Start from the data map if you have one. If not, use this list and add your own:
- Primary database: every table with a user reference, including ones without a foreign key, such as JSON columns, audit tables and denormalised copies.
- Read replicas and caches: Redis, Memcached, CDN-cached API responses.
- Search indexes: Elasticsearch, OpenSearch, Algolia, Typesense.
- Object storage: uploads, generated documents, data exports, CSV dumps, ad-hoc analyst files.
- Warehouse and lake: raw event tables, snapshots, dbt models, reverse-ETL syncs.
- Product analytics and marketing tools: event and profile data, ad audiences, push notification tools.
- CRM, support desk and email tools: contacts, tickets, attachments, email logs.
- Logs and error trackers: application logs, request logs, error and exception events with user context.
- Queues: dead-letter queues, retry queues and stream retention windows.
- ML and AI systems: training sets, feature stores, embeddings, prompt and response logs.
- Copies of production data: staging and dev databases, test fixtures.
- Backups and snapshots: database backups, disk snapshots, bucket versioning.
Step 2: Create a test user with unique, searchable values
Use values you can search for anywhere, with no false positives:
Name: Erasure Test 7f3a9c
Email: erasure-test+7f3a9c@yourcompany.com
Phone: your team's test number
City: Testpur 7f3a9cThen use the product the way a real user would. Sign up through the app, complete a purchase or a lesson, upload a file, contact support, open the marketing emails and trigger an error if you can. The goal is to push the test user's data everywhere real users' data goes.
Step 3: Wait, then take a "before" picture
Wait for the pipelines to run: ETL jobs, nightly snapshots, vendor syncs. Then search every location from step 1 and record where the test user appears. If they don't appear somewhere you expected, either the data doesn't go there or your search doesn't work. Find out which, before you go further.
For a PostgreSQL database, this block searches text, JSON, array and citext columns in tables and views for the marker:
-- Illustrative: full scans of every matching column. Run it on a replica or a
-- restored snapshot, not on the primary at peak time. Materialized views
-- aren't listed in information_schema, so check those separately.
DO $$
DECLARE
col record;
hits bigint;
BEGIN
FOR col IN
SELECT table_schema, table_name, column_name
FROM information_schema.columns
WHERE (data_type IN ('text', 'character varying', 'character', 'json', 'jsonb', 'ARRAY')
OR udt_name = 'citext')
AND table_schema NOT IN ('pg_catalog', 'information_schema')
LOOP
EXECUTE format(
'SELECT count(*) FROM %I.%I WHERE %I::text ILIKE %L',
col.table_schema, col.table_name, col.column_name, '%7f3a9c%'
) INTO hits;
IF hits > 0 THEN
RAISE NOTICE '%.%.%: % rows',
col.table_schema, col.table_name, col.column_name, hits;
END IF;
END LOOP;
END $$;For AWS CloudWatch Logs, a Logs Insights query across your log groups does the same job:
fields @timestamp, @log, @logStream, @message
| filter @message like /7f3a9c/
| sort @timestamp desc
| limit 100Select every relevant log group and a time range that covers the test. Search for the test user's internal user ID and device ID too: well-behaved logs record IDs, not names and emails, and those IDs still identify the person.
For vendors, use their user search or export features, and take screenshots or save the API responses. That record is your evidence later.
Step 4: Delete through the real flow
Trigger deletion the way a user would: the in-app button, or a request through your published channel. Don't run a manual DELETE and call it a test. The point is to test the pipeline, including every job, webhook and vendor API call it is supposed to make.
Step 5: Wait for your stated window, then search again
Wait for the period you promise users, then repeat every search from step 3. Record each location as pass or fail, with the query, the result count and the time. A fail with evidence is more useful than a pass you can't show.
The results usually look something like this:
| Location | Result | Why |
|---|---|---|
postgres.users, orders, addresses | Pass | Covered by the deletion job |
postgres.support_notes | Fail | No foreign key, so the job didn't know about it |
| Search index | Fail | The document wasn't removed when the row was |
warehouse.events_raw | Fail | Append-only table with no deletion process |
s3://exports/crm/ | Fail | Monthly CSV exports have no expiry |
| Product analytics tool | Fail | The vendor's user-deletion API was never called |
| Error tracker | Fail | Email captured in error context |
| Database backups | Documented | Expire after the retention window; see below |
Illustrative results, not from a client.
Step 6: Deal with backups
You usually can't edit a backup, and the Act and Rules don't mention backups. Three approaches are common, and many teams combine them. Agree yours with your lawyer:
- Short, fixed retention. If backups expire after a set window, deleted data ages out on a known schedule. Write the window down, and say it in your privacy notice or your response to the request.
- Re-apply deletions on restore. Keep a list of erased user IDs and nothing else. Make re-applying it a mandatory step in your restore runbook, before restored data goes back into use. An ID is still personal data, so restrict access to the list and expire entries once no backup could contain that user.
- Crypto-shredding. Encrypt each user's sensitive fields with a per-user key, and delete the key at erasure. Every copy of the encrypted data, including copies in backups, becomes unreadable. This only works if the keys live in a separate key store whose own backups don't keep deleted keys, and it doesn't cover fields you didn't encrypt.
Make it a regular check, not a one-off#
A deletion pipeline breaks every time someone adds a table, a vendor or an export. Three habits keep it working:
1. Fail the build when a new table isn't covered. Keep a deletion plan that maps each table with a user reference to a strategy, and test it:
// Illustrative: every table with a user reference must have a deletion strategy.
import { deletionPlan } from "../src/privacy/deletion-plan";
import { db } from "../src/db";
test("every table with a user reference is covered by the deletion plan", async () => {
const { rows } = await db.query(`
SELECT DISTINCT table_name
FROM information_schema.columns
WHERE table_schema = 'public'
AND column_name IN ('user_id', 'customer_id', 'student_id', 'patient_id')
`);
const missing = rows
.map((row: { table_name: string }) => row.table_name)
.filter((table: string) => !(table in deletionPlan));
expect(missing).toEqual([]);
});2. Run the test user through again on a schedule, in staging after every release and in production now and then, and alert on any fail.
3. Treat vendors like tables. When someone adds a new tool that receives user data, add its deletion API call to the pipeline at the same time. Most analytics, CRM and support vendors offer one, often built for GDPR. Keep the job IDs they return as evidence.
Keep evidence without keeping the data#
For each deletion request, record:
- when the request arrived
- how you verified the requester
- which jobs ran, and against which systems
- the job IDs your vendors returned
- the verification result
Don't record the deleted personal data itself. An internal user ID and the outcome are enough. That ID is still personal data, so keep the record for a stated reason, restrict who can see it, and give it an expiry date.
Time limits matter too. The DPDP Rules cap the response time of your grievance system at 90 days (Rule 14(3)), and the government's explainer says access, correction and erasure requests must also be answered within 90 days. A pipeline you have tested makes that easy. A manual process across a dozen tools usually doesn't.
Sources#
- The Digital Personal Data Protection Act, 2023, sections 6, 8 and 12 (MeitY)
- The Digital Personal Data Protection Rules, 2025, Rules 6, 8 and 14 and the Third and Seventh Schedules (MeitY)
- DPDP Rules, 2025 Notified, explainer, 17 November 2025 (PIB)
Not legal advice. This article explains engineering practice and quotes the Act, the Rules and CERT-In's directions for context. Decisions about your obligations need a lawyer. In our engagements, legal questions go to a partner law firm, and we give them our evidence.
Free spot check
See what a scan of your own systems finds before you commit to anything.
One repo plus one day of logs or one app build, read-only. You get a one-page map of the personal data we find and a walkthrough with an engineer.