diff --git a/README.md b/README.md
index 712da35..fee1d1a 100644
--- a/README.md
+++ b/README.md
@@ -90,6 +90,7 @@ The topics will be inferred by the browser. The browser will leverage a classifi
* e.g., site A might see topic ‘cats’ for the user, but site B might see topic ‘automobiles’. It’s difficult for the two to determine that they’re looking at the same user.
* The beginning of a week is per-user, per-site, and per-epoch. That is, for the same user, site A may see the new week's topics introduced at a different time than site B, and for the same user and site, the duration of a topic may not be exactly one week. This is to make it harder to correlate the same user across sites via the time that they change topics, or via the time interval between two changes.
* Not every API caller will receive a topic. Only callers that observed the user visit a site about the topic in question within the past three weeks can receive the topic. If the caller (specifically the site of the calling context) did not call the API in the past for that user on a site about that topic, then the topic will not be included in the array returned by the API. The exception to this filtering is the 5% random topic, that topic will not be filtered.
+ * Each epoch is automatically deleted within four weeks. The exact deletion time is per-user, per-site, and per-epoch.
* Note that observing a topic also includes observing the topic's entire ancestry tree. For instance, observing `/Arts & Entertainment/Humor/Live Comedy` also counts as having observed `/Arts & Entertainment/Humor/` and `/Arts & Entertainment`.
* This is to prevent the direct dissemination of user information to more parties than the technology that the API is replacing (third-party cookies).
* Example:
diff --git a/spec.bs b/spec.bs
index d16f0c9..c4ecab6 100644
--- a/spec.bs
+++ b/spec.bs
@@ -168,6 +168,9 @@ spec: html; urlPrefix: https://www.rfc-editor.org/rfc/
When the [=user agent/configuration version=] is updated, the browser must properly migrate or delete data in [=user agent/user topics state=] and [=user agent/topics history storage=] so that the state and the configuration are consistent.
+
Expiring stored data
+ User agents must automatically delete stored data 28 days after its creation.
+
The {{BrowsingTopic}} dictionary is used to contain the IDL correspondences of [=browsing topics types/topic id=], [=browsing topics types/version=], [=browsing topics types/configuration version=], [=browsing topics types/taxonomy version=], and [=browsing topics types/model version=].
@@ -363,6 +366,10 @@ spec: html; urlPrefix: https://www.rfc-editor.org/rfc/
1. Let |epochSwitchTimeDecisionHmacOutput| be the output of the [=HMAC algorithm=], given input parameters: whichSha=SHA256, key=user agent's [=user agent/user topics state=]'s [=user topics state/hmac key=], and message_array=|epochSwitchTimeDecisionMessageArray|.
1. Let |epochSwitchTimeDecisionHash| be 64-bit truncation of |epochSwitchTimeDecisionHmacOutput|.
1. Let |epochSwitchTimeDelayIntroduction| be a [=duration=] of (|epochSwitchTimeDecisionHash| % 172800) seconds (i.e. 172800 is 2 days in seconds).
+ 1. Let |epochPhaseOutTimeDecisionMessageArray| be the concatenation of "epoch-phase-out-time-decision|", |lastEpochTime|, and |callerContext|'s [=topics caller context/top level context domain=].
+ 1. Let |epochPhaseOutTimeDecisionHmacOutput| be the output of the [=HMAC algorithm=], given input parameters: whichSha=SHA256, key=user agent's [=user agent/user topics state=]'s [=user topics state/hmac key=], and message_array=|epochPhaseOutTimeDecisionMessageArray|.
+ 1. Let |epochPhaseOutTimeDecisionHash| be 64-bit truncation of |epochPhaseOutTimeDecisionHmacOutput|.
+ 1. Let |epochPhaseOutTimeOffset| be a [=duration=] of (|epochPhaseOutTimeDecisionHash| % 172800) seconds (i.e. 172800 is 2 days in seconds).
1. Let |timestamp| be |callerContext|'s [=topics caller context/timestamp=].
1. Let |result| be an empty [=list=].
1. Let |startEpochIndex| be -1.
@@ -375,14 +382,16 @@ spec: html; urlPrefix: https://www.rfc-editor.org/rfc/
1. Set |endEpochIndex| to |numEpochs| − 1.
1. If |endEpochIndex| ≥ 0:
1. Let |i| be |startEpochIndex|.
+ 1. Let |epochRetentionDuration| be a [=duration=] of 28 days.
1. While |i| ≤ |endEpochIndex|:
+ 1. If |epochs|[|i|]'s [=epoch/time=] < |timestamp| - |epochRetentionDuration| + |epochPhaseOutTimeOffset|, then continue.
1. [=list/Append=] |epochs|[|i|] to |result|.
1. Set |i| to |i| + 1.
1. Return |result|.
- This roughly returns 3 recently calculated epochs, either counting back from the last epoch, or from the second to the last epoch. The decision depends on whether some fixed duration (between 0 and 2 days, sticky to a user agent, site, and the last epoch) has passed since the last epoch was calculated. This essentially adds a per-site per-epoch fixed delay to the epoch switch time, to make it harder to correlate the same user across sites via the time that topics are changed, or via the time interval between two changes. The HMAC helps to compute the per-site per-epoch delay on the fly, without needing to store extra data for each site or epoch.
+ This roughly returns 3 recently calculated epochs, either counting back from the last epoch, or from the second to the last epoch, and excludes any epochs that are too old. The new epoch is introduced after a fixed duration (between 0 and 2 days) has elapsed since the epoch's calculation time. Each epoch expires after a longer fixed interval (between 26 and 28 days). Both durations are specific to each user, site, and epoch. This mechanism makes it harder to correlate the same user across sites via the time that topics are changed, or via the time interval between two changes. The HMAC helps to compute the per-user per-site per-epoch delay on the fly, without needing to store extra data for each site or epoch.