The deletion bug that kept Cloud App storage locked

Managed hosting has an awkward promise to keep: when you delete something, the panel should tell the truth immediately. Not eventually. Not after a support ticket. Immediately.
We found a case where it did not.
A customer had increased the storage on two Cloud Apps, then deleted them. The apps were gone, but 6 GB of storage still appeared as used in their capacity pool. Their remaining app needed only 1 GB, while the pool reported 7 GB in use. The customer was right to call it out.
What actually happened
Cloud App capacity is an envelope: CPU, RAM and storage are purchased once, then shared by the apps inside it. CPU and RAM are released when an app stops. Storage stays reserved while an app exists, because deleting data just because a process is stopped would be a terrible surprise.
That means the capacity query needs a very clear distinction between stopped and deleted.
The deletion path marks an app with a deletion timestamp rather than immediately erasing its database record. That is intentional: it gives us an audit trail and lets asynchronous cleanup finish safely. The bug was a race between that deletion path and a power-state update. In one timing window, the app was marked deleted, then later recorded as stopped. The storage aggregate counted stopped apps but did not consistently exclude deleted ones.
The database still had the right deletion marker. Our capacity calculation simply failed to treat that marker as authoritative.
Why we treated this as more than a display bug
At first glance, this can look like a meter issue. It was not. A customer who cannot reclaim purchased capacity cannot deploy another app, resize one, or accurately decide whether they need a larger subscription. A wrong capacity number becomes a billing and product decision problem.
We fixed the affected reservation, then looked for every other way the same lifecycle gap could matter. The important rule became simple:
A deleted app is never capacity-consuming, regardless of its last recorded runtime status.
The fix
We made three changes.
- Every capacity aggregate now explicitly filters out deleted app records. This is the safety belt. A later state transition cannot make a deleted app billable or capacity-consuming again.
- State updates now refuse to overwrite a deletion. Cleanup and power workers can finish their own work, but they cannot resurrect an app in the accounting view.
- We added regression coverage around the delete/power race. The useful test is not only “delete an app.” It is “delete an app while another worker is still trying to record its final state.”
We also checked for other stale deleted records after the fix and corrected the affected customer’s pool. No customer action was required.
What this changes for customers
When an app is deleted, its CPU, RAM and storage are released from its subscription accurately. Stopping an app still keeps its storage reserved, because its data is still there. That distinction is now enforced in the accounting layer rather than inferred from a status label.
It is a small detail, but it is one of those details that decides whether a control panel feels trustworthy. Your capacity page should be a source of truth, not a rough estimate that needs a human to reconcile it.
The broader lesson
Lifecycle states are easy to describe and hard to make correct under real timing. “Running,” “stopped,” and “deleted” are not just labels for a UI badge; they drive where data lives, what a customer can deploy, and what we are entitled to bill for.
We will keep writing about these failures when they are useful. Not because mistakes are a feature, but because the guardrails added after a mistake are part of the product too.
Learn how Cloud Apps capacity works, or create an account when you are ready to deploy.