The wrong delete  / field guide
Practitioner field guide · 17 October 2026 · Reliability & Operations

The delete lands one level up, on the path nobody guarded

Seven published deletions, from Atlassian, Microsoft, Google, Cloudflare, AWS, GitLab and Spotify, read side by side with the deletion designs of Meta, Netflix, GitLab and Kubernetes. None of the seven was a customer deleting their own data. Every one ran on the operator's own housekeeping path, and in four of them the delete hit the container of the thing it was aimed at. After reading, an architect can say where their system's undo window lives, whether it covers the path their own engineers use, and how long a restore takes compared with the delete that needs it.

34 graded sources 21 organisations 7 published deletion incidents Evidence through October 2026 Read: 35 min
01

The territory

Removing something on purpose, at scale, without removing the thing above it or beside it, and being able to put it back when you do.

883
Atlassian sites deleted in 23 minutes; some customers were out for up to 14 days
17
Production databases removed when a database-delete call became a server-delete call
1 year
Default term a blank field silently assigned, after which a customer's private cloud was deleted
~2/day
Deletion edge types Meta's developers mis-annotate; over-deletion twice as common as over-retention

This guide was picked because the evidence matured in the last eighteen months. GitLab published a design document in April 2025 that rebuilt its whole deletion flow and wrote down the alternative it rejected; it raised its undo window from seven to thirty days on 10 July 2025. Microsoft made Key Vault soft delete impossible to switch off in February 2025. Cloudflare disabled its own R2 storage gateway in February 2025 while trying to take down one phishing bucket, and Netflix described a deletion platform at QCon San Francisco in November 2025. Older incidents (AWS S3 in 2017, GitLab in 2017, Atlassian in 2022, Azure DevOps in 2023, Google Cloud in 2024) supply the failures. Read together, they say something none of them says alone.

The finding that surprised me

The safeguards existed. They were attached to the wrong caller. Google's report says it in one sentence: no notification was sent because the deletion came from an operator's blank parameter, "not due a customer deletion request. Any customer-initiated deletion would have been preceded by a notification." Atlassian's script carried a recoverable "mark for deletion" mode for normal operations and a permanent mode beside it. Azure customers "cannot restore Azure SQL servers themselves". In every incident here, the undo machinery protected the customer's path and the outage came through the operator's.

The second pattern is about where the delete lands. In four of the seven incidents the operation hit the parent of its intended target: an app became its site at Atlassian, a database became its server at Microsoft, one bucket's endpoint became the whole R2 gateway at Cloudflare, and a few billing servers became part of the S3 index fleet at AWS. I am calling this scope inflation; none of the sources names it, and each describes it as a typo or a wrong list. It is not random. Identifiers for parent and child are usually the same shape, both are valid inputs to the same API, and the credential that may delete the child almost always may delete the parent.

Figure 1 · Where seven deletions left the intended path

1 Intent: remove X

2 Identifier: which X

3 Scope: X and what else

4 Mode: undoable or final

Executed

Google GCVE 2024
blank field became a term

Atlassian 2022
site IDs for app IDs,
permanent mode

GitLab 2017
primary for secondary

Azure DevOps 2023
server for database

Cloudflare 2025
service for endpoint

AWS S3 2017
larger server set

1 Intent: remove X

2 Identifier: which X

3 Scope: X and what else

4 Mode: undoable or final

Executed

Google GCVE 2024
blank field became a term

Atlassian 2022
site IDs for app IDs,
permanent mode

GitLab 2017
primary for secondary

Azure DevOps 2023
server for database

Cloudflare 2025
service for endpoint

AWS S3 2017
larger server set

Every destructive operation passes four checkpoints, and each published incident failed at a different one. The failures cluster at identifier and scope, which are the two checkpoints most tools leave to the operator. Reconstructed from the postmortems cited in section 04.
Diagram source

Scope. This guide covers deliberate destructive operations in production: deleting tenants, resources, data and capacity, by people, scripts and scheduled jobs. It does not cover backup and restore drills (the earlier guide in this series, "A backup is a claim, a restore is the proof", does), privacy-driven erasure law, ransomware, or deletion by attackers.

02

How a safe delete is actually built

Seven components recur across Meta's DelF, Netflix's deletion platform, GitLab's 2025 blueprint, the cloud key and project services, and the remediations the incidents produced. No single system has all seven.

The common shape separates the request to delete from the act of destroying. A request moves the target into a pending state, which hides it, stops it being used, and keeps its bytes. A separate, slow, rate-limited reaper destroys what has sat in that state past a window. Everything that makes deletion safe happens in the gap between those two events, and every incident in section 04 is a case where some path skipped the gap.

Figure 2 · The reference shape of a recoverable delete

Destroy path: slow

Request path: fast

after window

restore

Delete request
typed target: kind plus id

Scope resolver
count, children, floor

Pending state
hidden, unusable, intact

Reaper
rate limited, low priority

Destroyed

Restoration journal
outlives the window

Live again

Notifier
fires on the reaper path

Audit loop
finds resurrected data

Destroy path: slow

Request path: fast

after window

restore

Delete request
typed target: kind plus id

Scope resolver
count, children, floor

Pending state
hidden, unusable, intact

Reaper
rate limited, low priority

Destroyed

Restoration journal
outlives the window

Live again

Notifier
fires on the reaper path

Audit loop
finds resurrected data

The request path and the destroy path are different processes with different owners and different speeds. The dashed boxes are the components the incidents show most systems lack: a scope check that sees children, and a journal that outlives the window. Reconstructed from DelF, Netflix's QCon 2025 talk, GitLab's 2025 design document, AWS KMS and Google Cloud project documentation.
Diagram source

Typed target

The delete names a kind and an identifier, and refuses an identifier of the wrong kind. Atlassian's script accepted a list of IDs and could not tell site IDs from app IDs. DelF annotates every object and edge type with how deletion should propagate, so the framework, not the caller, decides what goes with what.

Seen at: Meta DelF; absent at Atlassian 2022

Scope resolver

Before anything moves, compute what the request covers, including children, and compare it with a floor or a ceiling. AWS's post-2017 tool refuses removals that would take a subsystem below its minimum capacity. Kubernetes' interactive delete previews the resolved list rather than asking per item.

Seen at: AWS S3, Kubernetes KEP-3895

Pending state on the container

The undo window lives on the unit one command can destroy: a project, group, key, vault or site. GitLab moves "the item and all its child items" into pending deletion together; AWS KMS holds keys in PendingDeletion for 7 to 30 days; Google Cloud projects sit in DELETE_REQUESTED for 30.

Seen at: GitLab, AWS KMS, Google Cloud

Slow reaper

Destruction runs asynchronously, at a capped rate, below live traffic. Netflix treats deletes as low-priority requests; AWS's remediation made its capacity tool remove servers "more slowly". Slowness is the safeguard: it turns a fat-fingered list into an alert before it becomes an outage.

Seen at: Netflix, AWS

Restoration journal

DelF writes a write-ahead "restoration log" for every deletion and has used it to recover from deletions of "the wrong top-level object" in tens of incidents. Netflix runs journal and recovery services. A journal is what lets you undo a row-level mistake without soft-deleting every row.

Seen at: Meta, Netflix

A copy outside the deleted thing

UniSuper's private cloud came back because backups in a separate Cloud Storage location "were not impacted by the deletion". Spotify's clusters came back because its backups had already been restored once in a drill. A backup inside the container dies with it.

Seen at: Google/UniSuper, Spotify

The seventh component, a notifier, is the one Google's report makes central. A notification sent when the request is made is useless when the request is a blank field filled in a year earlier. The notification that matters fires on the reaper path, just before destruction, to the owner of the thing, whoever asked. Google does not say whether its corrected system now does this; it says it "corrected the system behavior that sets GCVE Private Clouds for deletion for such deployment workflows" and deprecated the internal tool. That is a fix to the trigger, not to the notifier.

Where the systems diverge is the granularity of the pending state. Meta and Netflix delete billions of rows and journal them; GitLab, AWS, Google and Microsoft put the window on large containers and let rows inside them go. Atlassian's 2022 remediation tried to do both, committing to universal soft deletes so that customer data "that has not gone through a soft-delete process cannot be deleted globally". Section 03 argues the divergence is not taste; it follows from which unit a single command can destroy.

Figure 3 · The lifecycle an operator should see in the dashboard

delete request

restore by owner

window expires, reaper runs

permanent mode, the outage path

restoration log kept

undo within log retention

log expires

Live

Pending

Destroyed

Journaled

delete request

restore by owner

window expires, reaper runs

permanent mode, the outage path

restoration log kept

undo within log retention

log expires

Live

Pending

Destroyed

Journaled

The arrow that causes outages is the one from Live straight to Destroyed. Atlassian's script exposed it as an execution mode; GitLab's personal-namespace projects had it as their only path until 2025; KMS and Key Vault removed it entirely. Sources as for Figure 2.
Diagram source
03

The decisions that matter

Five forks, each with the option that lost and the condition that would make it win.

Where does the undo window live: on every row, or on the container?

Chosen
  • Containers: GitLab projects and groups, KMS keys, Google Cloud projects, Key Vaults, Atlassian sites
  • Row-level mistakes covered by a journal (DelF restoration logs, a deleted_record table)
Rejected
  • A deleted_at column with a default scope on every model
  • Brandur: in ten years "never once" used to undelete; foreign keys "effectively lost"
Flips when
  • Many tenants share the rows, so the unit one command destroys is a set of rows, not a container. That was Atlassian's position, and it is why its remediation said universal.

The row-level argument is the best documented disagreement in this corpus, and it resolves cleanly once you ask what the soft delete is for. Brandur Leach's 2022 essay, written from Heroku and Stripe experience, says the deleted_at column costs a predicate on every query and the protection of foreign keys, and that in more than ten years nobody he worked with used it to undelete anything. The discard gem's README is written by someone who maintained the most popular Rails soft-delete library and concluded "it does the wrong thing for most cases", because a soft destroy still fires dependent destroys on the children. Django closed the request to own soft delete as wontfix in 2011 (#16567), and in 2022 closed a bug where a soft-delete default manager made the admin unable to restore rows (#33375). Three ecosystems, one finding: a row-level soft delete that is woven into every read path breaks the restore path it exists to serve.

Atlassian went the other way, and for a reason the essay does not cover. Its stores were multi-tenant, so restoring one customer meant extracting their rows from a point-in-time copy that also held everyone else's, which is why restoration ran in batches of up to 60 tenants and took four to five elapsed days each. The rule I draw from both: put the undo window on the unit a single command can destroy. If that unit is a project, soft-delete projects. If one command can remove a tenant's rows across shared tables, those rows need either a tenant-level pending state or a journal keyed by tenant, and a restore tool that works at that granularity.

Guard the command, or guarantee the recovery?

GitLab 2017 chose recovery
  • "Our main focus is to improve disaster recovery ... instead of preventing production engineers from running certain commands"
  • Plus a red production prompt (issue #1094)
Cloudflare 2025 and AWS 2017 chose the guard
  • Cloudflare removed the disable option from the abuse team's interface and restricted the Admin API
  • AWS added a rate limit and a capacity floor to the tool
Flips when
  • The operation destroys availability rather than data. Recovery bounds the damage of a data delete; it does nothing for the 59 minutes a disabled gateway is down. Guard availability-destroying operations; make data-destroying ones recoverable.

These look like opposite philosophies and are not. GitLab's argument was that aliasing rm protects against one command and "not against disk corruption or any of the many other ways you can lose data", so the durable investment is a restore you trust. That is right for data. Cloudflare's R2 incident and AWS's S3 incident destroyed nothing; they removed serving capacity, and the outage lasted exactly as long as it took to put it back. For those, the only lever is to stop the operation or slow it, which is what both remediations did. A design review should classify each destructive operation by which of the two it destroys before choosing the safeguard.

Should a bulk delete ask first, by default?

Kubernetes chose opt-in
  • kubectl delete --interactive, stable in v1.30, shows the resolved list once
  • Default confirmation "is out of the table" for backwards compatibility
Rejected
  • Default-on prompt for --all, "historically determined ... a breaking change"
  • Per-item yes-to-all, which means "you can delete the rest, even though I don't know"
Flips when
  • You own a new tool or API with no scripts depending on it. Then make the preview the default and the bypass the flag. Every year you wait, the compatibility argument gets stronger.

The Kubernetes record is worth reading for its honesty about why the safe default never shipped. The kubernetes-dev RFC says confirmation for --all was "a long requested feature" that the project kept declining because scripts would break. KEP-3895 cites "mistyping, hasty tab completions" as the motivating failures and settles for an opt-in flag. Its rejected alternative is the instructive part: a yes-to-all prompt was turned down because it asks for consent without showing scope. A prompt that does not display the resolved target list is not a safeguard; it is a speed bump that trains the operator to type y.

How long should the window be?

Chosen
  • GitLab: 7 days, then 30 on GitLab.com from 10 July 2025
  • KMS: 7 to 30, default 30; Key Vault: 7 to 90, default 90, fixed once set; Google Cloud projects: 30
Rejected
  • GitLab's alternative of admin-only early purge with a 30-day window, which it costed at "approximately fourfold" the storage
Flips when
  • Your time-to-notice exceeds the window. Set the window from the slowest plausible detection, not from storage cost, and when detection depends on a human noticing absence, add a notification at the reaper.

GitLab's design document is unusually frank about the cost. Pending items "still count towards usage quotas", and the rejected alternative, which would have stopped users purging early, was priced at roughly four times the storage and would have left users "unable to immediately and permanently remove sensitive data without admin involvement". GitLab took the longer window but kept user purge. The window length is really a detection budget. Atlassian's customers noticed within hours; Google's customer could not have noticed a deletion that was scheduled a year earlier by a default nobody knew existed. No window helps the second case. Only the notifier does.

Should the remediation codify the deletion?

The obvious move
  • Spotify, after deleting a 50-node production cluster, moved cluster lifecycle into reviewed infrastructure code
What it caused
  • Review builds "unknowingly modified global state"; "two PRs merged out of order"; a second deletion
Flips when
  • The code path has a plan step that runs without side effects and shows the deletions. Infrastructure as code is a safeguard only when removing a line produces a visible, reviewed destroy, which Terraform's own docs say prevent_destroy does not give you.

Figure 4 · Choosing the safeguard for one destructive operation

availability

data

a container

rows across tenants

yes

no, verified

Destroys data
or availability?

Guard it: rate limit,
capacity floor, remove
from roles that do not need it

Unit one command
can destroy?

Pending state on the container,
children follow parent

Tenant-keyed journal
plus tenant-granular restore

Can an operator path
reach it, not only customers?

Route operator path through
the same pending state,
notify at the reaper

Window at least the
slowest time-to-notice

availability

data

a container

rows across tenants

yes

no, verified

Destroys data
or availability?

Guard it: rate limit,
capacity floor, remove
from roles that do not need it

Unit one command
can destroy?

Pending state on the container,
children follow parent

Tenant-keyed journal
plus tenant-granular restore

Can an operator path
reach it, not only customers?

Route operator path through
the same pending state,
notify at the reaper

Window at least the
slowest time-to-notice

Ask first what the operation destroys, then who can reach it. Terminal nodes are actions. Derived from the decisions above; the branch points are mine, the evidence for each leaf is in the decision blocks.
Diagram source
DecisionChosenRejectedBecauseEvidence
Undo granularityContainer pending state; journal for rowsDefault-scoped deleted_atBreaks foreign keys, restore path, child cascadesBrandur 2022, discard
Guard or recoverRecover for data; guard for availabilityOne policy for bothRecovery does not shorten an availability outageGitLab 2017, Cloudflare 2025
Bulk confirmationOpt-in preview of resolved listDefault prompt; yes-to-allCompatibility; consent without scopeKEP-3895
Window length30 days, user may purge earlyAdmin-only purge~4x storage; privacy of early removalGitLab design doc 2025
Cascade orderWorkloads before the policies guarding themSemi-random namespace teardownPods outlived their NetworkPoliciesKEP-5080
04

What broke in production

Seven incidents in four classes. The class matters more than the company, because the class tells you which checkpoint in Figure 1 to fix.

Class A · Scope inflation: the delete landed on the parent

Four incidents, four organisations, one shape. The intended target and its container were both valid inputs to the same operation, under the same credential, and nothing compared the size of what was about to happen with the size of what was meant.

Postmortem

Atlassian: app IDs became site IDs, and the permanent mode was one flag away

AssumptionThe requesting team would hand over IDs of the legacy Insight app; the script's permanent mode would only be used deliberately.
What happened"Instead of providing the IDs of the intended app ... the team provided the IDs of the entire cloud site." The script ran "with the wrong execution mode and the wrong list of IDs" from 07:38 to 08:01 UTC on 5 April 2022.
Blast radius883 sites, 775 customers, up to 14 days. Restore was built for "single-site deletions", not a large set of interconnected sites; batches of up to 60 took 4 to 5 elapsed days.
FixUniversal soft delete as a precondition of global deletion; staged rollout and tested rollback for deletions; DR exercises restoring many sites.
Design ruleNever ship the recoverable and the permanent mode of a delete in the same tool. The permanent mode belongs to the reaper, which takes no list.
Postmortem (via press)

Azure DevOps: a library migration turned database-delete into server-delete

AssumptionSwapping deprecated Microsoft.Azure.Management packages for Azure.ResourceManager was mechanical, and Ring 0 would catch a behavioural change.
What happenedThe snapshot clean-up job's call "to delete the Azure SQL Database" became one "that deletes the Azure SQL Server". Ring 0 passed "due to the absence of any snapshot databases" old enough to trigger it.
Blast radiusAll seventeen production databases of the Brazil South scale unit; 12:10 to 22:31 UTC on 24 May 2023; noticed within 20 minutes; no data loss. Customers "cannot restore Azure SQL servers themselves".
FixReported: Azure Resource Manager locks on key resources.
Design ruleA clean-up job must be tested against a fixture where the thing it deletes exists. A delete path that saw zero targets in staging has not been tested.
SourceDCD, The Register, Techzine, all reporting Microsoft's postmortem, which retrieval could not reach
Postmortem

Cloudflare R2: disabling one phishing bucket disabled the gateway

AssumptionThe abuse team's product-disable action would act on the reported endpoint.
What happened"Human error and insufficient validation safeguards during a routine abuse remediation" disabled "the production R2 Gateway service responsible for the R2 API".
Blast radius59 minutes of failed R2 operations on 6 February 2025, spilling into Stream, Images, Cache Reserve, Vectorize and Log Delivery. No R2 data lost or corrupted.
FixReported: disable option removed from the abuse remediation interface; Admin API restricted so internal accounts cannot disable services.
Design ruleA role that acts on tenants should not hold a verb that acts on the platform. Separate the two by API, not by training.
Postmortem

AWS S3: a playbook input removed more servers than intended

AssumptionAn authorised engineer running an established playbook would remove a small number of billing-subsystem servers.
What happened"One of the inputs to the command was entered incorrectly and a larger set of servers was removed than intended", including servers of the index and placement subsystems.
Blast radiusS3 in us-east-1 from 9:37 AM PST on 28 February 2017; placement finished recovering at 1:54 PM PST, about 4 hours 17 minutes later (derived).
FixTool removes capacity "more slowly" and refuses to take a subsystem below its minimum; other operational tools audited for the same checks.
Design ruleA destructive tool should know the floor of what it acts on and refuse to cross it, whatever the input says.

Class B · The wrong sibling

Two incidents where the target was the right kind of thing in the wrong place. The fix in both lives in the operator's environment rather than in the API.

Postmortem

GitLab 2017: the wipe ran on db1, not db2

AssumptionThe engineer repairing replication was in a shell on the secondary.
What happenedThe engineer wiped "the PostgreSQL database directory, errantly thinking they were doing so on the secondary". The prompt showed only "db1" or "db2", in the same colours on staging and production.
Blast radiusAbout 300 GB removed before the process was stopped "a second or two" later; roughly six hours of production data lost because no backup restored.
FixRed, full-hostname production prompts (#1094); automated restore testing; an owner for data durability. Command blocking explicitly rejected.
Design ruleMake the environment unmistakable at the point of action, and invest the rest in a restore you have exercised, because the next wrong-host command will not be rm.
Talk

Spotify, told in 2019: a production cluster went where a test cluster should have

AssumptionThe cluster being deleted was not serving production. The slides place a test cluster beside three production regions; that the intended target was the test cluster is my reading of the slide sequence, not a stated fact.
What happened"I deleted a 50-node production cluster running dozens of workloads." Restoration "took 3.25 hours" because cluster creation scripts had bugs and the process "wasn't resumable, all or nothing".
Blast radiusNo end-user impact: teams had migrated only some instances of each service, and service discovery fell back to non-Kubernetes instances.
FixCodify cluster lifecycle, which produced the next incident (Class D).
Design ruleThe deletion you survive is the one where something else was already serving. Partial migration is a deletion safeguard nobody files under that name.

Class C · Deletion by default

Nobody asked for a delete. A default asked for it. This is the class the request-path safeguards cannot see, because there is no request.

Postmortem

Google Cloud and UniSuper: a blank field scheduled a deletion a year out

AssumptionAn internal provisioning tool used under "an exception process" in early 2023 would create a standing private cloud.
What happened"One input parameter was left blank"; "the system assigned a then unknown default fixed 1 year term value"; at the end of the term the GCVE private cloud was deleted. No notification, because the deletion did not come from a customer request.
Blast radiusOne customer's whole private cloud, May 2024; teams worked "24x7 over several days". Recovery came from backups in Cloud Storage that "were not impacted by the deletion" and third-party backup software.
FixInternal tool deprecated; all private clouds reviewed; the deletion-setting behaviour corrected.
Design ruleA blank must never mean "expire". Deletion-relevant fields need explicit values, and the notification belongs on the reaper path, where it catches deletions nobody requested.
Paper

Meta DelF: two mis-annotated deletion edges a day

AssumptionDevelopers can state how deletion should propagate along each relationship in a social graph.
What happenedThey mis-annotate "approximately 2 edge types every day", and mistakes causing inadvertent deletion are "approximately twice as common" as those causing retention. A deep edge where shallow was meant deletes things that should have survived.
Blast radiusNot quantified per incident; the paper reports "tens of incidents where user data was inadvertently deleted" and was recovered.
FixStatic and dynamic validation of annotations, heuristics on edge timestamps, and a restoration log for every deletion.
Design ruleCascade rules are code with a defect rate. Measure it, and keep an undo that outlives the time it takes to notice a wrong rule.

Terraform's documentation describes the same class as intended behaviour. Its prevent_destroy lifecycle flag "does not prevent the remote object from being destroyed if the resource block were removed from configuration entirely: in that case, the prevent_destroy setting is removed along with it." A protection stored inside the thing it protects disappears with it. The cloud services avoid this by storing the protection on the service side: Key Vault retention "can't be changed" once saved, and purge protection cannot be turned off.

Class D · The remediation that caused the next deletion

Spotify's second deletion is the only published case in this corpus, and it is enough to make the point. A month after the first incident the team was "trying to prevent accidental cluster deletions by codifying them"; review builds "unknowingly modified global state", and "two PRs merged out of order". Recreating the cluster then failed for lack of permissions, the team granted "enough but different permissions", and that changed Terraform's view of the clusters. The safeguard was a new path to the same delete, and it was less tested than the old one. The same pattern appears in a different domain in this series' guide on retry storms, where AWS's remedy for a 2014 outage sustained a 2015 one. Treat every deletion safeguard as a new destructive code path that needs its own test.

Figure 5 · How the Azure DevOps delete passed every gate

"Azure SQLengineers""ResourceManager""Snapshot clean-upjob""Ring 0""Sprint 222 change""Azure SQLengineers""ResourceManager""Snapshot clean-upjob""Ring 0""Sprint 222 change"12:10 UTC, noticed within 20minutespackage swap, delete call nowtargets the serverpasses, no snapshots oldenough to deleterolls to Brazil Southdelete old snapshot, sent asdelete serverserver and its 17 databasesremovedcustomers cannot restore a serverdatabases restored, backup configs aligned, 22:31 UTC
"Azure SQLengineers""ResourceManager""Snapshot clean-upjob""Ring 0""Sprint 222 change""Azure SQLengineers""ResourceManager""Snapshot clean-upjob""Ring 0""Sprint 222 change"12:10 UTC, noticed within 20minutespackage swap, delete call nowtargets the serverpasses, no snapshots oldenough to deleterolls to Brazil Southdelete old snapshot, sent asdelete serverserver and its 17 databasesremovedcustomers cannot restore a serverdatabases restored, backup configs aligned, 22:31 UTC
The safe-deployment ring worked as designed and proved nothing, because the delete path had no targets in Ring 0. The restore needed a different team. Reconstructed from press accounts quoting Microsoft's postmortem; the step order is theirs, the gate labels mine.
Diagram source
05

Numbers you can plan against

The figure that matters most is not in any single source: deletes run at thousands of times the speed of the restores that undo them.

MetricValueAtContextAs ofSource
Sites deleted by one script run883 in 23 minAtlassian07:38 to 08:01 UTC2022-04PIR
Restore batch and elapsed time60 sites, 4 to 5 daysAtlassianBefore parallel batches2022-04update
Delete-to-restore speed ratio~4,000x (estimate)Atlassian38 sites/min deleted vs 60 sites per ~4.5 days restored; derived2022-04derived from PIR and update
Outage after server-level delete10 h 21 minAzure DevOps17 databases; detected in 20 min2023-05DCD
Gateway disabled by abuse action59 minCloudflare R2No data lost2025-02Cloudflare
Capacity removal to placement recovery~4 h 17 minAWS S39:37 to 1:54 PM PST; derived2017-02AWS
Data removed before the wipe was stopped~300 GBGitLab"a second or two" of rm2017-01postmortem
Cluster restore time3.25 hSpotify50-node GKE cluster, tested backups2019-05slides
Silent default before deletion1 yearGoogle GCVEBlank parameter2024-05Google
Deletions executedbillions per dayMeta DelFThree social networks2020-08Meta
Mis-annotated edge types~2 per dayMeta DelFOver-deletion 2x over-retention2020-08paper
Rows deleted by central platform76.8 billionNetflix1,300 datasets, zero data loss (presenters' figures)2025-11InfoQ
Pending-deletion window7 to 30 daysGitLab.comChanged 10 July 2025; ~4x storage for the rejected variant2025-07GitLab
KMS key waiting period7 to 30 days, default 30AWSCancellable while pending2026AWS docs
Key Vault retention7 to 90 days, default 90AzureFixed once set; mandatory since Feb 20252025Azure docs
Project recovery period30 daysGoogle CloudCloud Storage and Pub/Sub delete "much sooner"2026Google docs

The asymmetry row is my arithmetic and should be read as an order of magnitude. Atlassian's script removed 883 sites in 23 minutes, about 38 a minute. Its restore, before it learned to run batches in parallel, returned 60 sites every four to five days, about one site every two and a half hours. The ratio is in the thousands. Azure shows the same shape in a smaller system: one API call, ten hours back. Any capacity plan for recovery that assumes restore is roughly as fast as delete is wrong by three orders of magnitude, and the reason is structural: deletion needs only the identifier, while restoration needs the identifier, the bytes, the configuration, the permissions and every system that referenced the thing.

Read these carefully

The Azure DevOps figures come from three trade outlets quoting Microsoft's postmortem, which this session could not retrieve; they agree with each other. Atlassian's customer count varied between its own documents (775 in the PIR, "approximately 400" in an earlier update); the PIR is later and is used here. Netflix's 76.8 billion is the presenters' figure as reported by InfoQ. No public source gives the cost of a pending-deletion window in currency; GitLab's "fourfold" is a relative storage figure for one rejected design.

What nobody has published. No organisation in this corpus reports how often its undo window is used on the operator path versus the customer path, which is the number that would test this guide's main claim. No postmortem reports a destructive operation that a pending state caught in production; the systems that have one publish their design, not their saves. DelF's "tens of incidents" is the closest, and it does not break them down.

06

The evidence wall

Every source behind this page, graded. Sources marked [search] sit on hosts this session's network denied and were read through retrieval; see sources.md.

PostmortemAtlassian2022-04

Post-Incident Review on the Atlassian April 2022 outage [search]

A decommission script ran with site IDs instead of app IDs and in permanent mode; 883 sites deleted; restore tooling built for single sites.

Carry forwardBuild the restore at the granularity of the largest delete a single command can perform.
atlassian.com/blog/atlassian-engineering/post-incident-review-april-2022-outage
PostmortemAWS2017-03

Summary of the Amazon S3 Service Disruption in US-EAST-1 [search]

A mistyped playbook input removed index and placement servers; the fix slowed the tool and gave it a capacity floor, then audited every other tool.

Carry forwardRate and floor are properties of the tool, not of the operator's care.
aws.amazon.com/message/41926
PostmortemGoogle Cloud2024-05

Sharing details on a recent incident impacting one of our customers

A blank parameter in an internal tool assigned a one-year term; the private cloud was deleted without notification because the customer had not asked.

Carry forwardPut the notification on the destroy path, so it fires for deletions nobody requested.
cloud.google.com/blog/products/infrastructure/details-of-google-cloud-gcve-incident
PostmortemGitLab2017-02

Postmortem of database outage of January 31

Wrong-host wipe of the primary; GitLab chose recoverability and clearer prompts over blocking commands, and said why.

Carry forwardThe argument for recovery over guardrails holds for data, not for availability.
gitlab.com/gitlab-com/www-gitlab-com (2017 postmortem source)
PostmortemCloudflare2025-02

Cloudflare incident on February 6, 2025 [search]

An abuse-remediation action meant for one phishing endpoint disabled the R2 Gateway for 59 minutes.

Carry forwardTenant-scoped roles must not hold platform-scoped verbs.
blog.cloudflare.com/cloudflare-incident-on-february-6-2025
Postmortem (press)Microsoft via DCD2023-06

Microsoft Azure outage in Brazil caused by typo [search]

Reports Microsoft's postmortem: the snapshot clean-up job deleted the SQL server and its 17 databases; 12:10 to 22:31 UTC. The primary was not retrievable in this session.

Carry forwardTest delete paths against fixtures where the targets exist.
datacenterdynamics.com/en/news/microsoft-azure-outage-in-brazil-caused-by-typo
Postmortem (press)Microsoft via The Register2023-06

Azure DevOps outage in Brazil [search]

Names the change: deprecated Microsoft.Azure.Management packages replaced by Azure.ResourceManager.

Carry forwardA dependency migration is a semantic change to every destructive call it touches; review those calls by hand.
theregister.com/2023/06/03/microsoft_azure_outage_brazil
Design docGitLab2025-04

Group and project deletions

Unifies pending deletion across tiers and namespace types; children follow the parent; rejects admin-only purge at about four times the storage.

Carry forwardWrite the rejected alternative and its cost into the design; it is the part a reviewer needs.
gitlab.com/gitlab-com/content-sites/handbook (group_and_project_deletions)
SourceGitLab2025-05

Epic 17208: Down-tier delayed deletion to core

Protection against accidental deletion had been a paid-tier feature; this epic extended it to free projects and groups to cut recovery tickets.

Carry forwardDeletion safety sold as a premium feature becomes a support cost on the tiers without it.
gitlab.com/groups/gitlab-org/-/epics/17208
SourceGitLab2025-07

Epic 17375: Extend pending deletion period from 7 to 30 days on SaaS

Closed 10 July 2025; lists storage impact analysis and performance testing as requirements.

Carry forwardLengthening a window is a capacity change; plan it like one.
gitlab.com/groups/gitlab-org/-/epics/17375
KEPKubernetes2023-03

KEP-3895: Interactive flag to kubectl delete

Default confirmation ruled out by compatibility; opt-in preview shipped stable in v1.30; yes-to-all rejected as consent without scope.

Carry forwardA confirmation must show the resolved target list or it is not a safeguard.
github.com/kubernetes/enhancements keps/sig-cli/3895
KEPKubernetes2024-01

KEP-5080: Ordered Namespace Deletion

Namespace teardown was semi-random, so pods could outlive the NetworkPolicies guarding them; now pods go first. Stable in v1.34.

Carry forwardDelete the guarded thing before the guard. Cascade order is a security property.
github.com/kubernetes/enhancements keps/sig-api-machinery/5080
SourceKubernetes2023

[RFC] Protecting users of kubectl delete [search]

States that confirmation for --all was long requested and repeatedly declined as a breaking change.

Carry forwardShip the safe default on day one; it only gets harder.
groups.google.com/g/kubernetes-dev/c/y4Q20V3dyOk
Sourcediscard (Ruby)2026

README: Why not paranoia or acts_as_paranoid?

A former paranoia maintainer: default-scope soft delete "does the wrong thing for most cases", and soft destroys still cascade dependent destroys.

Carry forwardHide rows explicitly; do not emulate deletion by overriding delete.
github.com/jhawthorn/discard README
SourceDjango2011, 2022

Tickets #16567 (soft delete, wontfix) and #33375 [search]

The framework declined to own soft delete; a soft-delete default manager later blocked restores in the admin.

Carry forwardTest the restore path whenever you change what the default read path hides.
code.djangoproject.com/ticket/33375
PaperMeta, EPFL, Georgia Tech2020-08

DelF: Safeguarding deletion correctness in Online Social Networks [search]

Annotated deletion semantics, static and dynamic validation, and restoration logs used in tens of recoveries; measures the human error rate in cascade rules.

Carry forwardJournal every deletion; cascade rules have a measurable defect rate.
usenix.org/conference/usenixsecurity20/presentation/cohn-gordon
PaperGunawi et al.2016-10

Why Does the Cloud Stop Computing? (SoCC 2016) [search]

A 597-outage corpus from public reports, 2009 to 2015, classifying root causes and fixes. Used here only to establish that the broad record exists; its deletion-specific breakdown was not retrieved.

Carry forwardThe incident corpus is large; deletion incidents are a minority that cost disproportionately in recovery time.
ece.iastate.edu (SoCC 2016 PDF)
TalkSpotify2019-05

How Spotify Accidentally Deleted All Its Kube Clusters with No User Impact

Two production cluster deletions; the second caused by the attempt to codify against the first; survived by partial migration and tested backups. Claims cited by slide number.

Carry forwardTreat a deletion safeguard as a new destructive path that needs its own test.
KubeCon EU 2019 keynote slides (PDF)
TalkNetflix2025-11

Architecting a Centralized Platform for Data Deletion at Netflix [search]

QCon SF 2025, 49:41: deletion as a platform with audit loops, tombstone control and resurrection detection.

Carry forwardDeletion has a failure mode in both directions; audit for data that came back.
infoq.com/presentations/architecting-deletion-system
TalkGoogle2019-10

Zero Touch Prod (SREcon19 EMEA slides) [search]

Names ad-hoc human production changes as an outage class ("I never make tipos") and argues every change be automated, pre-validated, or break-glass.

Carry forwardThe exception path is where the next deletion comes from; GCVE 2024 used one.
usenix.net srecon19emea_slides_wolafka.pdf
Eng blogMeta2020-08

DELF: Safeguarding deletion correctness in online social networks [search]

Billions of deletions a day, each with a restoration log that allows undo "for a limited time period".

Carry forwardA journal makes row-level undo possible without row-level soft delete.
engineering.fb.com/2020/08/12/security/delf
Eng blogbrandur.org2022-07

Soft deletion probably isn't worth it [search]

deleted_at costs a predicate on every query and the foreign keys; never used to undelete in ten years; proposes a deleted_record table.

Carry forwardIf you need row undo, journal the row out of the live table.
brandur.org/soft-deletion
Eng blogbrandur.org2024

Deleted record insert (fragment) [search]

Follow-up: the author's team moved off deleted_at except on a couple of core tables.

Carry forwardThe position survived contact with a production codebase.
brandur.org/fragments/deleted-record-insert
Eng blogGitLab2023-04

Delayed deletion [search]

Delayed deletion made default for paid tiers in 16.0 because support kept recovering accidental deletions.

Carry forwardSupport tickets for recovery are the leading indicator that the window is missing.
about.gitlab.com/blog/delayed-deletion
Eng blogGitLab2025-07

Improving GitLab deletion flow [search]

30-day window live on GitLab.com from 10 July 2025; admin-initiated deletions still to be routed through it.

Carry forwardEven here, the operator path joined the window last.
about.gitlab.com/blog/improving-gitlab-deletion-flow
Eng blogAtlassian2022-04

April 2022 outage update [search]

Restore in batches of up to 60 tenants, four to five elapsed days per site.

Carry forwardMeasure restore throughput in units per day before you need it.
atlassian.com/engineering/april-2022-outage-update
Eng blogAtlassian2022-08

August update on April 2022 outage PIR progress [search]

Four months on, the first disaster-recovery exercise for site deletion was still scheduled for the last quarter of 2022.

Carry forwardA restore capability for the large delete takes quarters to build; build it before the delete.
atlassian.com/blog/announcements/pir-update-august2022
AnalysisThe Pragmatic Engineer2022-04

Inside the Longest Atlassian Outage of All Time [search]

The dissenting read: Atlassian published a postmortem but would not give the number of affected users.

Carry forwardWeigh a vendor's postmortem against what it leaves out.
newsletter.pragmaticengineer.com/p/scoop-atlassian
Book chapterGoogle SRE2016

Data Integrity: What You Read Is What You Wrote [search]

Soft (lazy) deletion as the first defence layer, ahead of backups and validation.

Carry forwardThe undo window is the cheapest layer; backups are the expensive one.
sre.google/sre-book/data-integrity

Absent tier: no closed-unmerged pull request on deletion safety was retrievable through this session's network policy. The nearest rejected-approach records are Django's wontfix tickets, KEP-3895's rejected alternatives, the kubernetes-dev RFC and GitLab's rejected admin-only purge.

07

Build a miniature, then productionise it

Six rungs, from a table with a pending state to a deletion path you have attacked on purpose.

Pending state on one container

In a toy service with projects and child records, make DELETE move a project to a pending_deletion state with a scheduled purge time. Children are hidden through the parent, not by their own flag.

Done when: a deleted project's children vanish from every read and come back intact on restore.  Teaches: why the window belongs on the container, not on each row.

A reaper that takes no list

Write the purge as a separate job that selects only items past their window, at a capped rate. Remove every other code path that can hard delete.

Done when: grep finds exactly one hard-delete call site.  Teaches: Atlassian's lesson: the permanent mode should not accept caller input.

Typed targets and a scope check

Give project and organisation IDs different prefixes, reject the wrong kind, and make every bulk delete print the resolved count and refuse above a ceiling.

Done when: passing an org ID to the project delete fails before any write.  Teaches: scope inflation is catchable at the type, not in review.

Journal for row-level deletes

For rows deleted inside a live project, copy them as JSON to a deleted_record table keyed by tenant before removal, with expiry.

Done when: you can restore one tenant's deleted rows without touching anyone else's.  Teaches: the restore granularity Atlassian lacked.

Notify at the reaper, and test the operator path

Send the owner a notice when the reaper is about to purge, regardless of who requested it. Then delete something through your admin tool, a migration and a clean-up job, and check that each lands in pending state.

Done when: no path, including your own scripts, reaches Destroyed without passing Pending and a notice.  Teaches: Google's finding that safeguards follow the caller unless you move them.

Measure the asymmetry, then attack it

Time a bulk delete of 1,000 containers and the restore of all 1,000. Then run a game day: swap an identifier list, blank a retention field, upgrade the client library under the clean-up job against a fixture with real targets.

Done when: you have a measured delete-to-restore ratio and each injected fault was caught before the reaper.  Teaches: what your recovery capacity really is, which no design review tells you.

08

Keep hunting

The queries that found the material above. The useful vocabulary is "pending deletion", "restoration log", "delayed deletion" and the clean-up job's own name.

Incidents

  • post-incident review script deleted sites "mark for deletion" permanently delete
  • outage postmortem snapshot deletion job deleted "SQL server"
  • incident abuse remediation disabled service instead of endpoint
  • "remove a small number of servers" "larger set" postmortem
  • "accidentally deleted" production cluster keynote slides

Design records

  • "pending deletion" design document "alternative solutions" storage
  • KEP kubectl delete interactive "backwards compatibility"
  • "delayed deletion" epic site:gitlab.com
  • "ordered namespace deletion" NetworkPolicy KEP

The soft-delete argument

  • "soft deletion" "isn't worth it" deleted_at foreign keys
  • "Why not paranoia" default_scope soft delete
  • site:code.djangoproject.com soft delete wontfix manager
  • prevent_destroy "removed from configuration"

Deletion as a platform

  • "restoration log" deletion framework write-ahead undo
  • centralized data deletion platform tombstones resurrection talk
  • "lazy deletion" "soft deletion" data integrity defence in depth
  • ScheduleKeyDeletion waiting period PendingDeletion
09

References

  1. Atlassian, Post-Incident Review on the Atlassian April 2022 outageAtlassian, April 2022. Read via retrieval 2026-10-17.
  2. Atlassian, April 2022 outage updateAtlassian, April 2022. Read via retrieval 2026-10-17.
  3. Atlassian, August update on April 2022 outage PIR progressAtlassian, August 2022. Read via retrieval 2026-10-17.
  4. Gergely Orosz, Inside the Longest Atlassian Outage of All TimeThe Pragmatic Engineer, April 2022. Read via retrieval 2026-10-17.
  5. AWS, Summary of the Amazon S3 Service Disruption in the Northern Virginia (US-EAST-1) RegionAmazon Web Services, March 2017. Read via retrieval 2026-10-17.
  6. DatacenterDynamics, Microsoft Azure outage in Brazil caused by typoDCD, June 2023. Read via retrieval 2026-10-17.
  7. The Register, Azure DevOps outage in BrazilThe Register, 3 June 2023. Read via retrieval 2026-10-17.
  8. Techzine, Typo caused hours-long Microsoft Azure DevOps outageTechzine, June 2023. Read via retrieval 2026-10-17.
  9. Google Cloud, Sharing details on a recent incident impacting one of our customersGoogle Cloud, 24 May 2024. Fetched 2026-10-17.
  10. GitLab, Postmortem of database outage of January 31GitLab, 10 February 2017. Fetched 2026-10-17.
  11. GitLab, Issue #1094: Update PS1 across all hostsGitLab, February 2017. Fetched 2026-10-17.
  12. Cloudflare, Cloudflare incident on February 6, 2025Cloudflare, February 2025. Read via retrieval 2026-10-17.
  13. Hackmag, Failed attempt to block phishing link results in massive Cloudflare outageHackmag, February 2025. Read via retrieval 2026-10-17.
  14. David Xia, How Spotify Accidentally Deleted All Its Kube Clusters with No User ImpactKubeCon Europe 2019 keynote slides, 22 May 2019. Fetched 2026-10-17.
  15. Vidhya Arvind and Shawn Liu, Architecting a Centralized Platform for Data Deletion at NetflixQCon San Francisco / InfoQ, November 2025. Read via retrieval 2026-10-17.
  16. InfoQ, Netflix data deletionInfoQ, November 2025. Read via retrieval 2026-10-17.
  17. Rainer Wolafka, Zero Touch ProdSREcon19 EMEA slides, October 2019. Read via retrieval 2026-10-17.
  18. Cohn-Gordon et al., DelF: Safeguarding deletion correctness in Online Social NetworksUSENIX Security 2020. Read via retrieval 2026-10-17.
  19. Meta, DELF: Safeguarding deletion correctness in online social networksEngineering at Meta, 12 August 2020. Read via retrieval 2026-10-17.
  20. Gunawi et al., Why Does the Cloud Stop Computing? Lessons from Hundreds of Service OutagesACM SoCC 2016. Read via retrieval 2026-10-17.
  21. GitLab, Group and project deletions (design document)GitLab handbook, April 2025. Fetched 2026-10-17.
  22. GitLab, Epic 17208: Down-tier delayed deletion to coreGitLab, 2025. Fetched 2026-10-17.
  23. GitLab, Epic 17375: Extend pending deletion period from 7 to 30 days on SaaSGitLab, 2025. Fetched 2026-10-17.
  24. GitLab, Delayed deletionGitLab blog, 3 April 2023. Read via retrieval 2026-10-17.
  25. GitLab, Improving GitLab deletion flow: what to expect in coming monthsGitLab blog, 14 July 2025. Read via retrieval 2026-10-17.
  26. Kubernetes, KEP-3895: Interactive(-i) flag to kubectl deletekubernetes/enhancements, 2023. Fetched 2026-10-17.
  27. kubernetes-dev, [RFC] Protecting users of kubectl deleteMailing list, 2023. Read via retrieval 2026-10-17.
  28. Kubernetes, KEP-5080: Ordered Namespace Deletionkubernetes/enhancements, 2024. Fetched 2026-10-17.
  29. John Hawthorn, discard READMEGitHub, master. Fetched 2026-10-17.
  30. Django, Ticket #16567: Feature Req: Soft Delete and #33375Django Trac, 2011 and 2022. Read via retrieval 2026-10-17.
  31. Brandur Leach, Soft deletion probably isn't worth it and Deleted record insertbrandur.org, 2022 and 2024. Read via retrieval 2026-10-17.
  32. Google, Site Reliability Engineering, ch. 26: Data IntegrityO'Reilly / sre.google, 2016. Read via retrieval 2026-10-17.
  33. HashiCorp, The lifecycle Meta-Argument (Terraform v1.1)HashiCorp docs. Read via retrieval 2026-10-17.
  34. AWS, Schedule key deletionAWS KMS Developer Guide. Read via retrieval 2026-10-17.
  35. Google Cloud, Delete and restore projectsResource Manager docs. Read via retrieval 2026-10-17.
  36. Microsoft, Azure Key Vault soft-delete overview and Trend Micro, Key Vault purge protectionDocs, 2025. Read via retrieval 2026-10-17.