I deleted my account and the follow-up SAR found 47 orphan activity rows (PR #654 VIII F2)
Mikkel (30) is a data engineer at a Copenhagen logistics startup, fluent in privacy hygiene from a Schrems II audit at his previous job. 14 months he d ordered through thMenu QR at 19 Copenhagen + Aarhus restaurants. Deleted his thMenu account from Delete Account panel preparing for Berlin relocation — "Account will be fully erased within 7 days." 2 weeks later in his Mitte apartment, just-to-be-sure security paranoia, filed a GDPR Article 15 SAR on himself. 30 days later thMenu response: **"47 customer_activity rows still linked to your account — click-stream telemetry from your orders: product views, scroll depths, time-on-page. Profile deleted, but these rows orphaned."** Forensic: POST /api/customer/delete fired 7 days after Mikkel s request (TTL grace). Handler audit-log: "deletion_completed: status=partial_success, errors=[customer_activity: timeout (D1_SOCIAL upstream connection refused)]." 12 tables parallel DELETE via Promise.allSettled — some reject, others continue. cloudflare/src/handlers/customer-delete.ts: `const results = await Promise.allSettled(tables.map(t => env.D1_OPS.prepare(`DELETE FROM ${t} WHERE profile_id = ?`).bind(profileId).run())); ... return ok({ status: failures.length === 0 ? completed : partial_success });`. Sequential await + first-fail-throw cleaner (rollback) but 12 × 100ms = 1.2s vs parallel ~150ms performance compromise. Cross-D1 atomic transaction doesn t exist (D1_OPS + D1_SOCIAL + D1_MENU different instances). Engineering wrong theories: (1) sequential await — still partial across D1s; (2) per-D1 atomic batch — within-D1 fine but cross-D1 still partial. Right answer: 3-layer defense. **Layer 1 per-D1 atomic batch**: D1_OPS batch + D1_SOCIAL separate + D1_MENU separate, each atomic. **Layer 2 strict handler return shape**: parallel batch + each batch own promise; on failure return "partial" + frontend surfaces explicitly. **Layer 3 nightly orphan-sweep cron**: 04:00 UTC slot, paginated walk of every PII-bearing table s profile_ids + PostgREST cross-check against master truth (auth.users); missing rows orphan → DELETE. **PR #654 batch VIII F2** cron implementation: `pruneOrphanCustomerActivity` paginated 1000 row/page cursor-based walk + PostgREST batch `?id=in.(uuid1,...)` cross-check + Set diff for orphans + batch DELETE. cron_idempotency_claims dedup for 24h retry. First overnight run: customer_activity 3182 orphans, customer_email_links 218, customer_push_subscriptions 91, feedback (non-anon) 14 = 3505 total, ~847 unique customers, 18-month backlog. Mikkel follow-up email + 1-year free Pro tier gift. Pattern: **for distributed-DB delete handlers using Promise.allSettled (or parallel batches), catch the partial-fail silent residue with a nightly orphan-sweep cron as insurance layer.** Implementation checklist: (1) prefer per-D1 atomic batch + handler explicit partial-state return; (2) nightly cron walks PII-bearing tables + cross-checks master truth + DELETE orphans; (3) PostgREST .in. batched query single-fetch per page; (4) cursor-based pagination 1000/page worker memory safe; (5) cron_idempotency_claims dedup. Selin Istanbul version: fintech mobile engineer 11-month thMenu history, same follow-up SAR pattern.