All posts

Implementing Data Retention Without Breaking Your Reports

August 6, 20267 min read

The tension

GDPR's storage limitation principle says personal data should not be kept longer than necessary. Meanwhile, the most-requested analytics feature is year-over-year comparison.

These are reconcilable, because they want different things. Compliance is about row-level data that could identify someone. Reporting mostly wants aggregates, which do not.

Aggregate before you delete

Roll up first, then delete the detail:

INSERT INTO daily_stats (site_id, date, visitors, pageviews, sessions)
SELECT site_id, CAST(timestamp AS DATE),
       COUNT(DISTINCT user_id), COUNT(*), COUNT(DISTINCT session_id)
FROM events
WHERE timestamp < ? AND timestamp >= ?
GROUP BY site_id, CAST(timestamp AS DATE)
ON CONFLICT (site_id, date) DO NOTHING;

Now a raw event can be deleted after ninety days while the daily trend survives for years. The identifiers are gone; the counts remain. That is exactly the shape the regulation is aiming for.

Verify the rollup landed before deleting. Deleting first and discovering the aggregate failed is unrecoverable.

Per-site policies

Retention is not one number. A customer in a regulated industry may want thirty days; another may want two years. Store it per site:

CREATE TABLE data_retention_policies (
  site_id      VARCHAR(64) PRIMARY KEY REFERENCES sites(id) ON DELETE CASCADE,
  enabled      BOOLEAN NOT NULL DEFAULT false,
  retain_days  INTEGER NOT NULL DEFAULT 365
);

Default to disabled. Silently deleting data because a default was set is the kind of surprise that ends a customer relationship.

Delete in batches

A DELETE covering millions of rows holds locks and bloats the WAL:

DELETE FROM events
WHERE ctid IN (
  SELECT ctid FROM events
  WHERE site_id = ? AND timestamp < ?
  LIMIT 10000
);

Loop until no rows are affected. Slower in wall-clock terms, invisible to everyone using the system meanwhile.

The sweep

A scheduler that walks sites with an enabled policy every few hours is enough — retention is not urgent to the hour. Sites without a policy are untouched, so the loop is a no-op on most deployments.

One failing site must not stop the sweep. Catch per site, log, continue.

Visitor ID rotation

Retention alone is not the whole privacy story. Rotating the visitor identifier after a period of inactivity bounds how far back any single visitor can be correlated, even within the retention window.

A sliding window is the right shape: it resets on each visit, so a regular visitor is never re-counted mid-stream, and it only elapses after genuine absence.

Try InsightsTrack free

Open-source, self-hosted, privacy-first analytics — explore the live demo, no install required.

Open live dashboard