Short answer: private, S3-compatible object storage belongs in an app backup design only after a restore drill proves the retention policy, signed download flow, regional placement, and transfer rate against your recovery SLO; the lowest advertised storage rate is not enough evidence.
The failure pattern is familiar: a nightly job writes a database export and reports success, then an operator needs one tenant's data months later and discovers that the recovery path was never designed. The objects exist. The service still misses its recovery objective.
The incident lesson: a green backup job is not a recovery SLO
Consider a bounded production scenario. An application writes an export, uploaded media, and a manifest into a private bucket. The scheduler records a green run. During a later restore exercise, the team cannot identify the right prefix quickly, the transfer takes longer than the stated window, and authorization is handled by an improvised script. Nothing failed at upload time; the system failed at recovery.
That distinction should shape the design review. A recovery point objective says how much data loss is acceptable. A recovery time objective says how long restoration may take. The authorization boundary says who may start it and for which tenant. Capacity planning follows from those three statements, not from a storage-rate comparison.
Write down an invariant: no backup object is publicly readable; every restore is authorized by the application; every retained object has an expiry rule; and a representative dataset can be recovered inside the stated time. These are testable claims. A dashboard that only counts successful writes is not.
One sentence is enough here: recovery is a product surface.
Managed storage shifts disk replacement, replication, and hardware lifecycle away from the application team, while self-hosting keeps those duties on the platform on-call schedule. Both choices leave export correctness, credentials, retention policy, and restore drills with the team that owns the application. The buy-versus-build decision therefore depends on staffing and the durability SLO you can actually operate.
| Choice | Team owns | Useful fit | Catch |
|---|---|---|---|
| Self-hosted object store | Hardware, replication, upgrades, and capacity | A staffed platform team already runs storage | A storage incident competes with application recovery work |
| Managed S3-compatible service | Policy, credentials, exports, and drills | A small team needs an object API without storage hardware | Administrative limits and policy behavior still need acceptance tests |
| Replica or database snapshots | Lag, failover, and retention | Very short recovery objectives | It is not an independent copy for corruption or operator mistakes |
The catch is that object storage is a recovery medium, not a hot standby. Stick with a replica or snapshot failover path when the application must serve again in seconds.
How should private US/EU app backup storage handle retention and signed download links?
Treat US and EU placement as a data-governance decision, not a label on a pricing sheet. Define which tenants may have copies in each region, which operators may restore them, and whether a cross-region copy is permitted. Then test that a backup from one residency boundary cannot be selected by a workflow intended for the other.
Retention has two separate controls: when an object becomes eligible for deletion, and who can delete it before that time. Lifecycle rules can enforce ordinary expiry without relying on an application cron process. If policy requires an immutable interval, use the service's documented retention control and forecast the full retained volume before enabling it. Alert on unexpected retained bytes; a syntactically valid rule can still target the wrong prefix or version class.
Signed links preserve the private-bucket boundary when an operator needs a file. The application checks the actor's right to restore a specific tenant and backup, requests a time-bounded read URL, records the issuance, and returns it. The browser then talks directly to the object endpoint. That last hop matters: MDN notes that a cross-origin request can require a preflight, and the server must allow the exact origin, method, and headers used by the restore UI.
package restore import ( "context" "fmt" "time" ) type ObjectStore interface { PresignGet(ctx context.Context, bucket, key string, expiresAt time.Time) (string, error) } type Actor interface { CanRestore(tenantID string) bool ID() string } type Backup struct { TenantID string Bucket string Key string } type Audit interface { Record(ctx context.Context, event string, fields map[string]string) } func SignedRestoreURL(ctx context.Context, store ObjectStore, audit Audit, actor Actor, backup Backup, now time.Time) (string, error) { if !actor.CanRestore(backup.TenantID) { return "", fmt.Errorf("restore authorization denied") } expiresAt := now.Add(10 * time.Minute) url, err := store.PresignGet(ctx, backup.Bucket, backup.Key, expiresAt) if err != nil { return "", fmt.Errorf("create restore URL: %w", err) } audit.Record(ctx, "backup.restore_url_issued", map[string]string{ "actor_id": actor.ID(), "tenant_id": backup.TenantID, "object_key": backup.Key, "expires_at": expiresAt.UTC().Format(time.RFC3339), }) return url, nil } Ten minutes here is a policy input, not a universal setting. Set it from measured time to first byte, the operator workflow, and the exposure the organization accepts. Log the tenant, object, actor, and expiry; never log the signed URL itself.
What transfer and multipart tests should a backup restore pass?
Estimate the largest recovery set, add validation and import time, and work backward to the minimum sustained transfer rate. A 400 GB recovery set with a four-hour transfer budget needs about 28 MB/s before protocol overhead, retries, decompression, validation, and database import. A test that barely reaches that rate in a quiet environment has no incident margin.
Large exports need a tested multipart strategy. The multipart upload overview cited in References specifies a maximum of 10,000 parts, so set part size against the largest expected export rather than the median daily file. Keep a manifest containing object keys, sizes, checksums, export timestamps, schema version, and the application version that produced each export.
Concurrency is another capacity control. More workers can increase throughput until request limits, local disk, CPU for encryption or decompression, or the destination database becomes the bottleneck. Use a bounded worker pool and emit separate metrics for bytes transferred, failed objects, retries, time to first byte, verification time, and total recovery duration.
Run an acceptance suite against the exact capabilities the architecture needs: unauthenticated reads must be denied, uploads must be verified, a disposable prefix must expire according to policy, a signed download must work from the real browser, the largest test object must use multipart upload, and a complete restore must load into a disposable target. I've seen teams validate only the first two items, then discover during an incident that the browser's origin, the retained version class, and the import step were all outside the test. Keep the suite in deployment automation so a policy change has evidence attached to it.
What makes private backup storage cheap enough to operate?
Use a cost model that includes stored bytes, request volume for manifests and listings, network transfer during drills and incidents, duplicate copies for residency or independence, versioning, retention duration, and the human cost of operating storage. Include the growth curve, not just this month's byte count: a 30-day trial with a small tenant set can hide a year of retained versions, and a restore drill can create a transfer bill that never appears in the write path. A team that skips restore exercises may show a smaller bill while carrying an unpriced recovery risk.
Capacity is a constraint, not a footnote.
I'm not sure a generic price ranking stays meaningful once those variables move. Ask each candidate for the billing dimensions that map to the planned workload, then put documented terms into one forecast. Don't select on a headline rate before including US/EU placement and a full restore.
Keep an escape hatch. Use documented export formats, stable object names, manifests outside the application's only database, and authorization decisions enforced by the service rather than a browser convention. S3 compatibility can reduce migration work, but a compatibility label does not prove that lifecycle, retention, CORS, identity, audit export, and quotas behave identically.
The restore drill that earns confidence
Schedule a restoration exercise against a disposable target and treat its duration as an SLO observation. Select objects across age bands, including an older retained backup and the largest normal export. Confirm that direct unauthenticated reads fail, an authorized link expires, the browser flow follows the configured cross-origin policy, checksums verify, and the application can consume the recovered data.
Make the exercise boring enough to repeat. Recovery confidence comes from an ordinary procedure that works under measured constraints.
References
- MDN: Cross-Origin Resource Sharing (CORS): https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/CORS
- AWS S3 documentation: Multipart upload overview: https://docs.aws.amazon.com/AmazonS3/latest/userguide/mpuoverview.html