GDPR Article 17 gives users the right to have their personal data deleted. For applications that work with body measurement data, implementing this correctly is more complex than a single DELETE FROM users WHERE id = ?. Measurement data can be distributed across multiple tables, cached in external systems, logged in application logs, and held by third-party processors. A compliant erasure implementation must reach all of it.
What counts as personal data in a measurement context
First, a scope question: which data requires erasure under GDPR?
Personal data is any information relating to an identified or identifiable natural person. Body measurements — chest circumference, height, weight, waist size — are personal data because they relate to a specific person. When connected to a user account, they’re clearly in scope.
Special category data under Article 9 includes biometric data processed “for the purpose of uniquely identifying a natural person.” Storing a user’s body dimensions for sizing purposes is not biometric identification — the processing purpose matters. However, if you store body dimensions alongside other data that makes the person identifiable, you should treat the measurement data as Article 9 data and apply the higher standard of care regardless.
The practical rule: if deleting the measurement data wouldn’t leave behind enough information to re-identify the person, it’s not Article 9. If it would, treat it as Article 9.
What an erasure request covers
A valid erasure request requires deletion of:
- The user account and direct identifiers — email, name, account ID
- Measurement profiles — stored height, weight, body dimensions, regional parameters
- Derived outputs — size recommendations, stored predictions, fit profiles
- Associated records — order history referencing measurement data, fit feedback
- Backups — within a reasonable timeframe (typically “as soon as technically feasible” — this requires a documented retention policy)
- Third-party processors — you must notify downstream processors (analytics, customer support tools, etc.) of the erasure request
Database implementation
A cascade-delete approach handles the database layer cleanly:
-- Schema that supports compliant erasure via cascade
CREATE TABLE measurement_subjects (
id UUID PRIMARY KEY,
external_user_id TEXT UNIQUE, -- your app's user identifier
created_at TIMESTAMPTZ DEFAULT now(),
erasure_requested_at TIMESTAMPTZ, -- log when request was received
erasure_completed_at TIMESTAMPTZ -- log when all data was deleted
);
-- All child tables reference measurement_subjects with ON DELETE CASCADE
CREATE TABLE measurement_profiles (
id UUID PRIMARY KEY,
subject_id UUID NOT NULL REFERENCES measurement_subjects(id) ON DELETE CASCADE,
...
);
CREATE TABLE size_assignments (
id UUID PRIMARY KEY,
subject_id UUID NOT NULL REFERENCES measurement_subjects(id) ON DELETE CASCADE,
...
);
With cascade deletes configured, a single deletion at the parent level propagates through all child tables:
import psycopg2
from datetime import datetime, timezone
def process_erasure_request(user_id: str, db_conn) -> dict:
"""
Executes GDPR right-to-erasure for a user.
Returns a record of what was deleted.
"""
with db_conn.cursor() as cur:
# Log the erasure request timestamp first
cur.execute(
"UPDATE measurement_subjects SET erasure_requested_at = %s "
"WHERE external_user_id = %s RETURNING id",
(datetime.now(timezone.utc), user_id)
)
row = cur.fetchone()
if not row:
return {"status": "not_found", "user_id": user_id}
subject_id = row[0]
# Count records before deletion (for audit log)
cur.execute("SELECT COUNT(*) FROM measurement_profiles WHERE subject_id = %s", (subject_id,))
profile_count = cur.fetchone()[0]
cur.execute("SELECT COUNT(*) FROM size_assignments WHERE subject_id = %s", (subject_id,))
assignment_count = cur.fetchone()[0]
# Delete subject — cascades to all child tables
cur.execute("DELETE FROM measurement_subjects WHERE id = %s", (subject_id,))
db_conn.commit()
return {
"status": "completed",
"user_id": user_id,
"deleted_at": datetime.now(timezone.utc).isoformat(),
"records_deleted": {
"measurement_profiles": profile_count,
"size_assignments": assignment_count
}
}
Cache invalidation
Application-level caches (Redis, Memcached, CDN edge caches) are a common gap in erasure implementations. If you cache API responses keyed by user ID or by the user’s measurement inputs, those cached values must be invalidated.
import redis
import hashlib
def invalidate_user_cache(user_id: str, measurement_profile: dict, r: redis.Redis) -> None:
"""
Remove all cached predictions for this user's measurement profile.
Call before or during erasure processing.
"""
# Pattern: cache keys that include the user's ID directly
pattern = f"predict:user:{user_id}:*"
for key in r.scan_iter(pattern):
r.delete(key)
# Pattern: cache keys derived from measurement inputs (anonymous cache)
if measurement_profile:
key_data = {
"gender": measurement_profile.get("gender"),
"height_mm": measurement_profile.get("body_height_mm"),
"mass_kg": measurement_profile.get("body_mass_kg"),
}
import json
key_str = json.dumps(key_data, sort_keys=True)
hashed_key = f"predict:{hashlib.sha256(key_str.encode()).hexdigest()}"
r.delete(hashed_key)
Third-party processor notification
If you use third-party services that receive body measurement data — analytics platforms, customer support tools, A/B testing systems — your GDPR Data Processing Agreement (DPA) with each requires them to honor erasure requests. In practice, this means:
Analytics: Most analytics tools (GA4, Mixpanel, Amplitude) provide user deletion APIs. Call them programmatically as part of your erasure workflow.
Customer support: Support tickets may reference body measurement discussions. Flag affected tickets for manual review by your support team.
Email marketing: Ensure the user is unsubscribed and their profile deleted from your email platform.
def notify_third_party_processors(user_id: str, user_email: str) -> list[dict]:
"""
Call deletion APIs for third-party processors.
Returns list of results for audit logging.
"""
results = []
# Example: call your analytics platform's user deletion endpoint
# Replace with actual SDK calls for your specific platforms
results.append({
"processor": "analytics_platform",
"status": "deletion_requested",
"timestamp": datetime.now(timezone.utc).isoformat()
})
# Example: email platform unsubscribe
results.append({
"processor": "email_platform",
"status": "unsubscribed_and_deleted",
"timestamp": datetime.now(timezone.utc).isoformat()
})
return results
The stateless API advantage
If you use a stateless prediction API — one that accepts inputs, computes predictions, and returns results without storing any data on its end — your third-party processor obligations for the prediction API are minimal. There’s nothing to delete because nothing was stored.
This is a meaningful architectural advantage. A stateless API means:
- No erasure request to send to the API provider
- No data retention on the API side
- No DPA clause covering deletion timelines
- No risk of the provider’s storage persisting after your own deletion
The tradeoff is that you must re-call the API whenever you need predictions, rather than retrieving stored predictions. For most sizing applications, this is fine: predictions are generated at sizing recommendation time, not stored for later retrieval.
Logging the erasure itself
GDPR Article 5(2) requires demonstrability — you must be able to prove that you processed data lawfully, including that you honored erasure requests. Maintain an erasure audit log that is itself protected from modification:
def log_erasure_event(
user_id: str,
result: dict,
processor_results: list[dict],
audit_db_conn
) -> None:
"""
Write an immutable erasure audit record.
This table should never have UPDATE or DELETE permissions.
"""
with audit_db_conn.cursor() as cur:
cur.execute(
"""
INSERT INTO erasure_audit_log
(user_id, requested_at, completed_at, deletion_summary, processor_summary)
VALUES (%s, %s, %s, %s, %s)
""",
(
user_id,
result.get("requested_at"),
result.get("deleted_at"),
json.dumps(result.get("records_deleted", {})),
json.dumps(processor_results)
)
)
audit_db_conn.commit()
Note: the audit log contains the user identifier and a record of when their data was deleted — but not the deleted data itself. This is consistent with GDPR: you’re not storing the personal data, you’re storing evidence that it was deleted.
Timeline requirements
GDPR requires erasure “without undue delay” and “within one month” of a valid request. For the database layer, this is trivially achievable. The harder cases are:
- Backups: State-of-the-art practice is to exclude deleted users from backup restoration procedures (mark them as deleted, don’t attempt to restore their records from backup) rather than modifying existing backups, which is often technically impractical
- Logs: Application logs may contain measurement data from API request/response logging. Either avoid logging raw body measurement inputs in production logs, or implement log rotation that respects your one-month obligation
- Third-party processors: Notify them promptly and document your notification; their response timeline is governed by their DPA
The non-obvious part: children’s data
If your application serves users under 16 (the Article 8 age of digital consent in most EU member states), their data was processed under parental consent. When a parent or guardian submits an erasure request on behalf of a child, the same obligations apply — delete all measurement profiles, size assignments, and derived data tied to that child’s records. The implementation is identical; the trigger is different.
Erasure implementation is not glamorous engineering. But it’s a legal obligation with teeth — GDPR fines for non-compliance are meaningful, and regulators have specifically focused on data deletion failures. Getting the cascade deletes, cache invalidation, and third-party notifications right before launch is significantly easier than retrofitting them under enforcement pressure.