Unreal Ops Monitor

Retention and deletion

Three different things delete data here, on three different triggers, and they give back different amounts of what they took. This is all of them.

Last read against the code on 29 August 2026. Written by the person who wrote the software, from the software. No lawyer has reviewed it, and it contains no legal terms that one would have written.

Retention is a number your plan buys, and it is enforced

Not published and quietly ignored. A cron job deletes by it every night.

PlanEvents kept
Developer, the free plan7 days
Solo30 days
Studio90 days

The number is a property of the organization, not of a project, and it is resolved from the plan catalog on every sweep rather than copied into a row when you sign up. That means a change to a plan's retention reaches every account on that plan the next night, with nothing to migrate. An organization with no subscription row at all, which is what a self hosted deployment that sells nothing looks like, resolves the product default of 90 days.

Age is measured from when Monitor received the event, not from the timestamp the client put on it. A device clock can be days wrong in either direction, and a wrong clock must not be able to make an event immortal or delete it the moment it arrives.

What the nightly sweeper deletes

One Vercel Cron invocation, at 04:20 UTC, guarded by a shared secret.

  • Events past the organization's cutoff, oldest first.
  • Sessions past the same cutoff, by start time.
  • Daily issue buckets past the same cutoff, which is what the charts on an issue are drawn from.
  • Per minute rate buckets older than two days. These are only ever read for the current minute and for a quarter hour spike baseline, so anything older is dead the moment it is written.
  • Symbol indexes, on their own rule rather than by date: 90 days since the last crash on that build, the last time symbolication needed it, or its upload, whichever is latest. A build that is still crashing keeps its symbols however long ago it shipped.

When events are deleted, the issue they belonged to is repaired in the same transaction. The event count is recomputed from what survives rather than decremented, the people count is recomputed from the actors that still have an event, and an actor whose every event has aged out loses its row. An issue whose last event has gone is deleted outright, along with its actors, its daily buckets and its alerting history, because an issue with nothing behind it can only mislead. The one field left alone is first seen: it is a fact about the issue rather than a promise about what you can open, and it genuinely did start in March.

The run is bounded three ways, so it can neither time out nor run away: 45 seconds of wall clock, 20,000 rows in total, and 2,000 rows for any one project. The project list is rotated by the day number so the same few are not always swept first. When a budget stops it, the result says so and the next night continues, because "past the cutoff" is the only state it needs to remember. Running it twice is safe and does nothing the second time.

What is never swept: usage counters, daily usage, refusal counts, the release list and the record of which builds are missing symbols. They are one row per project per period, they cost almost nothing, and they are the last thing you should lose. How much an account used in March has to still be answerable in April.

Deleting a project does not delete anything

It is a reversible delete, and the reversible part is the point.

What changes immediately is ingest: the key lookup joins on the project not being deleted, so the next envelope is refused. What comes back is exactly one allowance, the plan's project count, because that is the only ceiling that counts live things rather than stored ones.

The monthly event pool and the pooled symbol storage do not come back, and that is deliberate rather than an oversight. Both of those measure rows that still exist and still cost what they cost. Retention sweeps a deleted project on exactly the same schedule as a live one, so its events sit on the same disk for up to ninety more days. Refunding the allowance would let an organization at its cap delete a project, get the month back, and keep ingesting against storage it had not released.

Restoring clears the flag and the project works again. The plan ceiling is rechecked when you do, because the organization may have filled the gap it left.

Purging is what actually releases storage

A separate act, not a flag on the first one.

Purge deletes the project's rows for real. The tables it clears are enumerated from the live database schema rather than from a list somebody maintains, so a table added later is included without anybody remembering to add it. The rows are drained table by table in chunks, outside any long transaction, and the project row itself goes last: if the run is interrupted, what is left is a project with fewer rows than it had, which is a delete to run again, rather than an orphan nothing can find.

It is bounded like the sweeper is, so a very large project can come back as "deleted this many rows and stopped". That is a success with more to do, not a failure; send it again. It is owner only and it asks you to type the project's slug back, which is enough friction that nobody does it in a loop by accident.

One consequence worth naming rather than discovering: the usage rows go with everything else, and the pooled monthly check is a sum over them, so purging a project hands its month back. That is honest about storage, which really has been released, and dishonest about the row writes, which were already spent and cannot be. Closing that properly needs a usage rollup that outlives its projects, which is a schema change and is not built.

Deleting an organization deletes everything beneath it

Including accounts. This is the most destructive thing in the product and nothing undoes it.

Owner only, and you type the organization's slug back. Every table carrying that organization is drained in chunks, then one small transaction takes the accounts and the organization row together. The organization row goes last and only when the drain finished, for the same reason a project row does, and deleting it fires a cascade that sweeps anything ingest managed to write while the drain was running.

Accounts go too, and only the ones with nowhere else to be. Somebody who signed up, made one organization and wants it gone is asking for their account to stop existing, and leaving the user row behind would leave an email address reserved against an account nobody can sign in to anything with. Somebody who is also a member of another organization keeps their account and simply loses this membership, because deleting them would be deleting somebody else's colleague. Their outstanding email verification goes with them.

Nothing is written to the audit log, because the audit log for that organization has just been deleted along with everything that named it. The response is the record, and the person who made the call is the only one left holding it. A large organization can exhaust the row budget before it is empty, which comes back as progress made and the same call to send again.

What a plan change does to data you already have

The direction people are surprised by is downward.

Because retention is resolved from the plan on every sweep rather than stored, moving to a shorter retention takes effect on the next nightly run, on events that already exist. An organization that drops from Studio to Developer loses everything older than seven days that night. Nothing warns you first and nothing keeps a copy.

The pooled symbol allowance behaves differently: it does not delete anything on its own, but the next upload that does not fit evicts the oldest release's symbols to make room, oldest first, and never the release currently being uploaded. Uploads with no release attached go first of all, because nothing can tell which build they belong to. Each upload response names what it evicted, so the fact lands in a CI log rather than on a screen nobody opens.

Stopping the sweeper

Because a housekeeping job misbehaving is its own kind of incident.

There is a switch that stops the background jobs, retention and the daily health check, without stopping ingest, and it can be flipped without a deployment. When it is armed the sweep deletes nothing and reports that it was stopped rather than returning zeros, because zeros are also what a healthy night with nothing to do returns and those are opposite situations. The consequence while it is armed is that data past its retention keeps existing, so it is an operator decision with a cost, not a pause button.