For a gaming service, the expensive storage decision is made on restore day. The backup may be written in one region, the operator may restore it in another, and the object count may turn a modest byte total into a very large request bill.
Short answer: compare Backblaze B2, Cloudflare R2, AWS S3, and Wasabi with the same private app backup dataset, the same EU or US restore destination, and a tested retention and deletion procedure; the result is defensible only when the complete recovery path satisfies the required recovery point and recovery time.
That answer is deliberately less tidy than a storage-per-gigabyte chart. A gaming backup usually contains a database export, tenant configuration, and player-generated objects. Losing the configuration while retaining the archive is a successful download and a failed recovery. I would record the recovery unit first, then measure the bill and the deletion boundary around it.
What should a gaming team measure before choosing private object storage for EU/US app backups?
Start with one named drill, not an average month. For example, define a selected snapshot for one tenant, its database dump, its object manifest, and the destination where the restored service will be validated. Record the storage region, restore region, bytes read, bytes transferred, request count, and any retrieval class that applies. Repeat the same shape for every candidate.
The object count matters as much as the capacity. A backup made of twelve large archives and a backup made of millions of small objects can occupy similar space while producing very different listing, retry, and checksum work. The latter also needs a resumable manifest: an operator should be able to restart after a process exit without silently skipping a page or beginning every download again.
EU and US are not labels to paste into a spreadsheet. They are constraints on the selected storage location, the restore worker, the destination database, and the logs that may contain object keys. A region choice does not prove that every recovery leg stays in the intended jurisdiction. Keep those boundaries in the drill record.
Private access needs its own test. The application worker writes objects with private access, an authenticated control-plane action authorizes a selected snapshot, and the restore worker receives only the capability it needs for that operation. If a browser downloads the result, the HTTP Content-Disposition response header can express download behavior rather than inline rendering; the header does not make an object public. A signed URL is still a bearer capability, so it should have a narrow lifetime and should not be written to ordinary logs. The MDN reference in the sources describes this header behavior.
Restore first.
Retention and deletion are separate failure boundaries
Retention is a promise about how long a selected snapshot remains recoverable. Deletion is a promise about what disappears, when it disappears, and what an operator can still recover after requesting removal. Treating both as a single “lifecycle policy” hides the dangerous cases.
For each tenant, give the snapshot an immutable logical identity and store its manifest separately from the data objects. The deletion worker should first mark the snapshot as pending deletion, check that a newer valid snapshot exists if the product requires one, and emit an audit event before removing objects. A retry must be idempotent. A partial deletion must be visible and resumable. The control plane must not report “deleted” merely because the first batch request was accepted.
This is where the common shortcut fails: a lifecycle rule can expire objects by age, but it does not by itself establish that the application can restore a selected tenant snapshot, prove what was deleted, or prevent a still-needed snapshot from being selected. The AWS lifecycle documentation describes rules applied to objects, which is useful evidence for the mechanism but not a substitute for an application-level restore record.
The invariants for this design are plain:
- A backup remains private until an authorized restore operation starts.
- A selected snapshot has a manifest, checksum policy, and recorded storage location.
- Retention evaluation and deletion are auditable, retryable, and safe to resume.
- A full restore is tested, including transfer, request volume, and validation of the recovered tenant.
The failure boundaries are not. An absent object is different from a throttled request; a bad checksum is different from a transient network error; and a deleted manifest is different from a deleted data object. Collapse those states and the runbook will tell an attractive lie.
A comparison record for the four storage candidates
The table is a test plan, not a ranking. Fill cost fields from current contracts during the drill. Do not compare one provider's byte retrieval charge with another provider's monthly capacity charge and call the result a restore cost.
| Candidate | Measure during the same drill | Retention and deletion question | Reject when |
|---|---|---|---|
| Backblaze B2 | Stored bytes, restored bytes, requests, transfer destination, and client behavior | Can the application prove snapshot state and resume a deletion safely? | The measured recovery or audit path misses a required invariant |
| Cloudflare R2 | The same object manifest, region assumptions, request shape, and restore worker | Can the team separate expiration from an operator-requested deletion? | The estimate relies on a pricing assumption that the current contract does not support |
| AWS S3 | Exact region, lifecycle rules, request categories, transfer, and retention controls | Which controls are provider policy and which are application state? | The design pays for or operates controls that the recovery requirement does not need |
| Wasabi | Capacity terms, complete restore, object count, transfer destination, and client compatibility | What evidence remains after a tenant deletion is retried or interrupted? | The workload's object shape or retention period conflicts with the current terms |
The fair comparison is the one that makes every candidate answer the same uncomfortable questions. Can the team restore one tenant without exposing another tenant's keys? Can it identify a snapshot that was selected before a deletion request? Can it explain a missing object to an auditor? Can it run the operation from a clean worker with credentials that are scoped to the task?
I would also run a deletion rehearsal against a disposable tenant. Capture the manifest before deletion, interrupt the worker between batches, rerun it, and verify both the resulting object state and the audit trail. That is a better signal than a successful upload because it exercises the primary decision axis: retention and deletion.
How should a team model app backup restore costs without hiding request shape?
The following Python model intentionally contains no vendor rates. It makes the inputs visible, keeps monthly storage separate from the recovery event, and lets a team add a separate line for request categories when its contract requires that detail. The numbers are scenario inputs, not a claim about any provider.
from dataclasses import dataclass
@dataclass(frozen=True)
class RestoreScenario:
stored_gb: float
restored_gb: float
object_count: int
storage_gb_month: float
retrieval_gb: float
transfer_gb: float
requests_per_1000: float
def monthly_storage(self) -> float:
return self.stored_gb * self.storage_gb_month
def restore_cost(self) -> float:
request_units = (self.object_count + 999) // 1000
return (
self.restored_gb * self.retrieval_gb
+ self.restored_gb * self.transfer_gb
+ request_units * self.requests_per_1000
)
def main() -> None:
scenario = RestoreScenario(
stored_gb=2048,
restored_gb=2048,
object_count=8_000_000,
storage_gb_month=0.0,
retrieval_gb=0.0,
transfer_gb=0.0,
requests_per_1000=0.0,
)
print(f"monthly storage: {scenario.monthly_storage():.2f}")
print(f"one restore: {scenario.restore_cost():.2f}")
if __name__ == "__main__":
main()
The zero values are placeholders that force a dated quote into the review instead of smuggling a stale number into source code. The 2,048 GB and 8,000,000-object shape is also only an example; replace it with the tenant distribution that the drill must represent. If the contract has minimum retention periods, tier transitions, taxes, support, or multiple request classes, put those terms beside this simple calculation rather than pretending one formula captures them.
The observability should follow the same dimensions: tenant, snapshot ID, region, object count, bytes, retry count, checksum failures, deletion state, and elapsed time. Never use an aggregate “restore succeeded” metric as the only signal. It can be true while one tenant's manifest is incomplete.
The rejected shortcut and the decision rule
The rejected shortcut is selecting the lowest advertised capacity price. It is valid only for disposable data whose recovery transfer, request count, retention evidence, and deletion behavior do not matter. App backups do not fit that description.
Choose the candidate whose complete drill satisfies the invariants with the fewest unowned failure boundaries. Keep a provider-native option in contention when its retention, deletion, residency, or compatibility controls are mandatory. Consider another option when the application can own those controls and the measured restore path is simpler to operate. The catch is that “S3-compatible” does not mean identical lifecycle semantics, request billing, or retention behavior; compatibility still needs a test against the actual backup library and runbook.
I'm not sure which candidate is cheapest before the object distribution, regions, restore frequency, and current terms are recorded. Your mileage may vary. The defensible decision is the one that survives a selected-snapshot restore and an interrupted tenant deletion, not the one with the nicest upload quote.













