18 min read

Your Age Check Has No Expiry Date. The EU's Has Three Months.

Age is the only attribute you verify that is guaranteed to become wrong, on a date you could have calculated at the moment you checked it. Yet almost every platform stores the result as a boolean with no expiry column, and the only cadence any regulator has actually mandated is an annual review of the service, not of the user. The EU age verification app already picked three months and no revocation. Here is what really expires in an age decision, why the four clocks are not the same clock, and how to set a re-verification cadence without rebuilding the birthdate you were told not to keep.

Editorial illustration on a deep slate-navy background: a row of stored age decision tokens sits behind a verification gate, each token fading from bright to grey along a horizontal time axis. One token in the middle has quietly flipped from a restricted marker to an unrestricted one, but the gate still reads the old value. Abstract, no faces, no people, no readable text.

The European Commission’s age verification app has an opinion about how long an age check stays true. It is three months.

The technical specification recommends that a Proof of Age attestation “remain valid for a maximum period of three (3) months from the date of issuance”, that an attestation presented as a plain ISO mDoc be treated as single use, and that attestations be issued in batches of roughly thirty so a user does not run out (EU Age Verification Blueprint, section 3.4.3). The same document declines to build revocation at all, on the grounds that revocation “is not required for age verification purposes” and would add a lot of complexity for little gain.

Read those two decisions together and you get a design position, stated plainly by the people building Europe’s reference implementation: an age credential is a perishable good, and the way you handle a stale one is to let it die of old age rather than to chase it.

Now open your own user table. There is very likely a column called something like age_verified or is_adult, it holds a boolean, and it has no sibling column holding a timestamp after which that boolean stops being trusted. It was written once, in 2025, by a check that ran for eleven seconds, and your product has read it as current fact several thousand times a day ever since.

That is the gap. Not a regulation you missed. A field you never added.

Age is the only attribute that is guaranteed to go wrong

Most identity attributes decay in a fuzzy way. An address goes stale when someone moves, and you have no idea when that is. A name changes rarely and unpredictably. A sanctions match can appear tomorrow or never.

Age is different in a way that ought to make engineers happy and instead makes them careless. It decays on a schedule. If you learned at verification time that a user was fifteen, you also learned the date on which they stop being fifteen. The information was in your hands. Most systems throw it away and then behave as though the answer is permanent.

And the decay is not symmetric. This is the part teams get backwards.

An adult decision never becomes false by arithmetic. Someone verified as over 18 in 2025 is still over 18 in 2030. No clock makes that wrong. If you are running an 18+ gate and nothing else, the passage of time is not your problem at all.

A minor decision always becomes false, on a date you can compute. Every user you classified as under 16 for the Australian rules, or under 18 for a teen-by-default experience, or under 13 for a COPPA path, has a date on which that classification stops being correct. Not “might stop”. Stops.

So the useful question splits in two. For adults, re-verification is a fraud and binding control: is the person using this account still the person who passed the check? For minors, re-verification is arithmetic: has the restriction I applied expired? Those two problems have different triggers, different costs, different failure directions, and almost nobody separates them.

What we see instead is the inverse of what the risk suggests. Platforms re-prompt adults every twelve months because a compliance slide said checks should be periodic, which produces friction, drop-off and support tickets for zero safety gain. And they never revisit the restricted accounts at all, which is the half where a date is actually ticking.

Four clocks, not one

“How long is an age check valid” is an unanswerable question because four different things expire and teams treat them as one thing.

Clock 1: the evidence. The passport expires. The mobile driving licence has a validity window. The proof of age attestation lasts three months and, in the EU model, cannot be revoked before then. If your decision was derived from a credential, the credential’s own lifetime is a hard ceiling on the age of the evidence, and it has nothing to do with the user.

Clock 2: the binding. The check proved that a person holding this device, at that moment, was over the threshold. It did not prove that the same person holds the device now. A family tablet, a sold account, a borrowed session and a child using a parent’s logged-in profile all break the link between the verified human and the current session without touching the stored result. We have written this one up separately in binding, shared devices and proxy verification. For adults, this is the only clock that matters.

Clock 3: the classification. The arithmetic clock. It only runs for users you placed below a threshold, and it runs to a date you could have written down.

Clock 4: the method. Your vendor shipped a new model version. You changed your threshold. The certification behind the method reached its renewal date, and note that Age Check Certification Scheme certificates run for two years, which is about the system and not about any individual user. A decision made under policy version 3 is not evidence about how the gate behaves under policy version 5.

These four have wildly different periods. Clock 1 is months. Clock 2 is continuous and event driven. Clock 3 runs to a fixed calendar date, per user. Clock 4 moves whenever you or your provider deploy. Collapsing them into a single “re-verify every 12 months” policy means you are always too slow for one of them and always too expensive for another.

Nobody told you the cadence, and that is the finding

It is worth being precise about what the law currently says, because the honest answer is: very little, and none of it at the user level.

Under the UK Online Safety Act, a service that concludes it is not likely to be accessed by children has to record that assessment and repeat it at least annually (Ofcom). Read the object of that sentence carefully. The thing being repeated annually is the children’s access assessment, which is a judgement about your service. It is not a requirement to re-run a check against any particular user. Ofcom’s four criteria for highly effective age assurance, technical accuracy, robustness, reliability and fairness, all describe the quality of a method at the point it runs. None of them describes how long its output stays good afterwards.

The General Data Protection Regulation gets closer. Article 5(1)(d) requires personal data to be accurate and, where necessary, kept up to date, and controllers must take every reasonable step to erase or rectify inaccurate data without delay. The Information Commissioner’s Office is explicit that there is no fixed frequency for this: it depends on how quickly the data goes stale and on what you are using it for (ICO).

Here is why that phrasing is uncomfortable for age data specifically. “How quickly does it go stale” is normally a matter of judgement. For an age band it is a matter of subtraction. You are not guessing whether a user’s stored classification is still right. You either computed the transition date or you deliberately chose not to. A regulator asking why a nineteen-year-old is still in a restricted teen tier is not asking you to predict the future. It is asking why you did not use a number you already had.

Article 16 then adds a clock of its own. A user who tells you their stored age is now wrong has a right to rectification, you have one month to respond, and you have to pass the correction on to anyone you shared the data with. If your age band sits in six downstream systems, including an ad platform and a recommendation service, that propagation is a real engineering requirement and not a policy sentence.

Australia is where the abstract version of this became an operational queue. Since the under-16 rules took effect, platforms removed roughly 4.7 million under-16 accounts in the first weeks alone, and the providers publicly committed to letting those users come back. Meta said it would hold the data until the user turns 16. Snapchat described suspending accounts for three years or until the user reaches 16, whichever comes first (Time, CNBC). Millions of accounts now have a scheduled reinstatement date. Somebody has to build the job that honours it, and that job is an age assurance control with its own failure modes, not a customer service nicety.

The trap: you cannot age someone up without the date you were told not to keep

Every privacy guide on age verification gives the same advice, and it is good advice. Do not collect a full date of birth when a threshold answer will do. Store the boolean, not the birthdate. Delete the document image the moment the check completes.

Follow that advice completely and you have made ageing up impossible. A boolean has no transition date in it. To know when a restricted user becomes unrestricted you need, at minimum, the date the restriction ends.

Now be honest about what that date is. If your only threshold is 18, then storing restriction_ends_at is storing the birthdate with a fixed offset. It is not a clever privacy trick. It is the date of birth wearing a hat. If you hold several thresholds, say 13, 16 and 18, and you store transitions for each, you have reconstructed the birthday exactly.

So the real decision is not “store a date or don’t”. It is which of four postures you take, and each one costs something specific.

Store nothing and re-check on access. Purest on minimisation, and it works if your check is cheap. The cost is friction on every re-check and a live dependency on your provider being up, which puts you straight into fail open or fail closed territory. Viable for a Check. Not viable if the re-check is a document scan.

Store a coarsened transition. Keep the month or the quarter in which the restriction lapses, not the day. A user whose restriction actually ends on 4 March is unrestricted on 1 April. You have lost precision in the direction that keeps a minor restricted for up to a few extra weeks, which is the safe direction to be wrong in. You have also stopped holding an exact birthday. This is the option most teams should take and almost nobody considers.

Hold the transition at the verification provider. Your database keeps a band and a review_after marker, the provider holds the precise transition and sends you an event when it fires. This does not delete the data, it relocates it, so it is only an improvement if the provider processes it under a narrow purpose and a real retention limit. It does mean the exact birthdate is out of your primary user store and out of your breach blast area, which is worth something. Ask the direct question during procurement, because plenty of vendors will happily keep it forever.

Store the full date of birth and justify it. Sometimes correct. If you operate several thresholds across several jurisdictions, you are going to reconstruct the date anyway, and pretending otherwise produces a worse system than documenting the necessity in your record of processing activities and applying proper controls to the field. Just do not arrive here by accident.

Setting the cadence

A cadence policy that says “annually” for everything is a policy that was written to fill a box. Set it per case, per clock, and write down which way each one fails.

Case Which clock runs Sensible review point Failure direction if late
Adult verified for an 18+ gate Binding only On risk signal, not on a timer Wrong person keeps access
User classified under 13 (COPPA path) Classification The month they turn 13 Over-restriction, and a stale consent record
User classified under 16 (Australia, teen tiers) Classification The month they turn 16 Account stays suspended past its lawful end
Restricted teen experience, 16 to 18 Classification The month they turn 18 Adult held in a minor’s tier, inaccurate data under Article 5(1)(d)
Decision made under an older policy or model version Method On deploy, by re-evaluating the cohort in shadow You are evidencing a gate you no longer run
Credential-derived decision (EU attestation, mDL) Evidence Credential validity window, three months in the EU model You are trusting an expired credential
Account recovery, device change, ownership change Binding At the event A transferred account keeps its age status

Two things fall out of that table that are worth saying directly.

First, the adult row has no timer in it. Re-verifying adults on a calendar is the single most common cadence policy in this industry and it is close to pure cost. It burns budget, it creates drop-off, and the thing it is nominally protecting against, someone else using the account, is not something a yearly prompt catches. Spend that effort on binding signals instead.

Second, the classification rows are all cheap. You are not running a model. You are comparing a stored date against today. The expensive part is not the check, it is what you do when the comparison flips.

The age-up job is a security boundary, not a cron script

Here is the attack, and it is obvious once stated, which is why it is worrying that so few age-up designs mention it.

A 14-year-old hits your age gate, self-declares a date of birth that makes them 15 years and 9 months old, and gets restricted. Three months later your scheduled job reads that stored date, sees a 16th birthday, and quietly lifts the restriction. Nobody checked anything. The child chose their own unlock date at signup and simply waited.

Any age-up transition that is driven by self-asserted data is a delayed bypass with a timer on it. The rule that fixes it is short: the evidence that lifts a restriction must be at least as strong as the evidence that applied it. If the restriction came from a real check, the lift can be automatic from that same check’s data. If the restriction came from a declared date, the lift needs a check.

That is the security half. The operational half is the mirror image of a problem we have written about at length in back-book remediation. Both are batch jobs that change the status of accounts in bulk based on a stored classification. The difference is the direction of harm, and it is a big difference.

A back-book sweep that gets it wrong deletes an adult’s account. There is often no undo. An age-up sweep that gets it wrong grants access early, which is a safety failure, or grants it late, which is a service failure you can fix by running the job again tomorrow.

That asymmetry should change how you build it. Run the age-up job daily rather than monthly, because the cost of running it often is trivial and the cost of a user waiting three weeks past their birthday is a support ticket and a bad review. Make it idempotent, so re-running is always safe. Log every transition into the same decision record stream as the original check, with the reason, the source date and the policy version, because “why did this account gain adult permissions on 14 March” is exactly the question an information request asks. And put a guard on the batch size: if today’s job wants to unlock five hundred thousand accounts, something is wrong with your date arithmetic and the job should stop and page someone rather than proceed.

That last point connects back to something worth its own paragraph. All the ordinary date bugs apply here and they apply at scale. A leap day birthday, a timezone offset that moves a birthday by a day, a date of birth stored as a timestamp and rendered in a negative-offset locale. On a single signup those bugs affect one user. In a nightly age-up sweep they affect a whole cohort in the same direction, every night, silently. If you write one property-based test this quarter, make it the one that checks your transition date arithmetic against an independent reference across a few million generated birthdates.

Cadence is a cost decision, so cost it

Re-verification has a price, and the price is the reason most teams either overdo it on the cheap population or skip it entirely on the expensive one.

Take a platform with 500,000 registered users and an annual refresh policy. That is roughly 41,700 events a month. Run it as a lightweight Check and, on our Growth plan at 199 EUR a month with 30,000 Checks included and 0.02 EUR per Check after that, the extra 11,700 events cost about 234 EUR a month. Run the same refresh as a full document Verification at 0.20 EUR each and you are looking at 100,000 EUR a year to re-establish something you already knew.

The number matters less than the ratio. A document verification is roughly ten times the price of a check, so a cadence policy that reaches for the document path by default is not slightly more expensive, it is an order of magnitude more expensive, and it is also the path with the worst completion rate. Cadence design and method selection are the same decision.

The shape that works: use the cheap path for the recurring case and reserve the expensive path for the exception. Returning-user lookups and age checks handle refresh, binding signals and the routine classification flip. Document verification is for the initial hard case, the step up after a failed or borderline estimate, and the appeal. Anything else and you are paying document prices for a calendar comparison.

How Xident is built for this

Three parts of our design exist because of this exact problem.

Two operations, priced ten times apart, on purpose. We separate a Check, which covers browser-based age checks, liveness, returning-user Xident ID lookup and OAuth, from a Verification, which is the document and ID path with optical character recognition and a face match. That split is what makes a real re-verification cadence affordable. A periodic refresh runs as a Check. A step up runs as a Verification. On Growth those are 0.02 EUR and 0.20 EUR, and the gap between them is the whole argument for not re-running document scans on a timer.

Decision records instead of retained documents. Every decision we return is stored as a record: outcome, method, threshold, confidence, policy version and timestamp. Not the passport image. That record is what a re-verification policy actually needs, because “when was this decided, by which method, under which policy version” is the input to every one of the four clocks above. A boolean cannot answer any of them. It is also the artefact you export when someone asks you to evidence a measure you had in use eleven months ago.

A sandbox sized for building the job, not for a demo. The free tier grants 1,000 Checks and 100 document Verifications as one-time allowances, not a monthly quota. The Verifications are there so you can exercise the document path end to end, including the failure branches, before you pay for anything. An age-up job with a step-up branch in it is exactly the sort of thing you want to have run against a real integration before it touches production accounts.

The short version

Age is the one attribute you verify that comes with its own expiry date attached, and the industry has built almost all of its tooling around the moment of the check rather than the life of the result.

Nobody has told you the cadence, and the reason is not that regulators forgot. It is that the only cadence anyone has mandated is a yearly look at your service, while the clock that actually ticks is per user and sits in data most privacy guidance told you to delete. That tension is real and it does not resolve by picking a side. It resolves by storing less precision than a birthday and more than a boolean, by putting the transition where it can be acted on, and by treating the moment a restriction lifts as a decision that deserves the same evidence as the moment it was applied.

Add the column. Write down which clock it belongs to. Then decide, on purpose, what happens when it runs out.


Xident provides age verification and age estimation infrastructure built around decision records rather than retained documents, with a cheap Check path for recurring re-verification and a separate document Verification path for step-up. The free sandbox includes a one-time allowance of 1,000 Checks and 100 document Verifications. Talk to us about the cadence your programme is missing.

Share this article

Ready to implement age verification?

Get started in minutes with our simple SDK. Free trial includes 100 verifications.

Book a 20-minute demo