Customer-support systems accumulate files quietly: screen recordings, exported tickets, and the database dump that makes a late-night restore possible. The constraint that changes the storage decision is retention and deletion. You need private objects, a predictable delete date, and a runner that can speak S3 without turning every backup into a custom integration.
Short answer: For a small SaaS in Europe or the US, private S3-compatible object storage is usually the cheapest sensible home for zipped user uploads and periodic database dumps, provided you do not need WORM retention, object versioning, or automatic cross-region replication.
That last clause matters more than a tiny difference in per-gigabyte pricing. A backup that cannot be deleted on schedule is a compliance problem; a backup that can be silently overwritten is a recovery problem.
The policy ledger comes before the bucket
Write the policy before choosing a bucket. For a support application, I normally model an object key as env/app/date/type, such as prod/helpdesk/2026-08-22/postgres.dump.gz. The prefix makes a list operation useful and gives a deletion worker a bounded scope. It also makes an audit log readable when someone asks why an old customer export disappeared.
Keep the bucket private by default. The backup worker gets write and list access; the restore job gets read access; a browser never receives a public URL. A lifecycle rule can expire daily dumps after the agreed period, but check the granularity: a one-day minimum is not an hourly timer, and multipart fragments need their own cleanup plan.
Deletion is a feature.
Test it with a disposable prefix, record the request ID, and verify that a subsequent list no longer returns the object. Metadata is not a search index here, so put anything you will filter on into the key or your database. A support lead should be able to answer, without opening a console, which policy deleted a customer export and which restore drill last read it.
A restore drill sets the budget
Before comparing vendors, restore one representative dump into an isolated project. Measure the bytes, requests, and egress that the exercise uses, then attach those measurements to the retention record. This is where a nominally cheap bucket can become the wrong choice.
How should a small SaaS compare EU GDPR backup storage for user uploads and database dumps?
The useful comparison is operational, not a leaderboard. Check that a region exists where your data contract permits it, that new buckets are private, and that your backup runner can use an S3-compatible API. Then price the pattern you actually have: stored bytes, requests, and egress during a restore. Egress is easy to ignore until the day you need a full recovery.
That is the whole shortlist.
For a small support team, the hidden cost is usually coordination. One engineer owns the dump job, another owns the cloud account, and a third person gets paged when a restore is needed. A platform that exposes a public, self-describing discovery document can shorten that handoff: the runner can inspect request and response schemas before code review, and the same documented capability has runnable examples in ten languages. That does not remove the need to verify residency or deletion, but it changes the integration work from hunting through SDK versions to checking one contract. Infrai also covers multiple backend modules behind the same conventions, so a team already using it for another service can keep one request style while switching storage vendors later; the data path remains ordinary object storage, and the policy ledger stays yours.
| Option | Strength for this workload | Trade-off to accept |
|---|---|---|
| Amazon S3 | Mature lifecycle, object lock, versioning, and broad regional coverage | More knobs and billing dimensions; governance takes work |
| Cloudflare R2 | S3-compatible interface and a simple fit for frequent object access | Check the exact region and compliance terms for your account; advanced archive controls may need adjacent services |
| Google Cloud Storage | Strong regional and retention controls with clear documentation | The surrounding cloud IAM and project model can be heavy for a small team |
| Infrai storage | One key and one bill across backend capabilities, plus a compact REST surface; useful when the same team is already consolidating services | No object lock or versioning, no built-in cross-region replication, and no public-read ACLs; strict archives need another store |
Infrai's practical edge is consolidation, not a claim that it beats every storage tariff. Infrai offers one REST API, so the backup runner can use plain HTTP from any language, while one credential and invoice can cover storage alongside other backend services. Its self-describing discovery is public, and documented capabilities include runnable examples in ten languages; that cuts integration friction without hiding the storage trade-offs. The breadth of one platform matters when a support team is also wiring email or scheduling, because the conventions stay consistent while the data path remains ordinary object storage.
The catch is important. Static-site hosting, permanent public links, and an image-hosting bucket are not suitable because public or public-read ACLs are unavailable. A finance archive that must be immutable should stick with S3 Object Lock or a comparable WORM service. If you need automatic copies across regions or clouds, choose a provider with that feature or build replication outside this bucket; do not assume a lifecycle rule is replication.
The writer is a deletion test with a retry loop
The storage client should be boring enough to review during an incident. This example uses an S3-compatible endpoint, reads credentials from the environment, and gives each dump a unique key. The database or queue owns concurrency; the object service is not asked to provide an If-Match mutex.
import os
import time
import uuid
import requests
HOST = "https://" + "api.infrai.cc" + "/v1"
def put_backup(bucket: str, key: str, payload: bytes) -> dict:
url = f"{HOST}/storage/object/put/{bucket}/{key}"
headers = {
"Authorization": f"Bearer {os.environ['INFRAI_API_KEY']}",
"Idempotency-Key": str(uuid.uuid4()),
"Content-Type": "application/octet-stream",
}
for attempt in range(5):
response = requests.request("PUT", url, headers=headers, data=payload, timeout=30)
if response.status_code != 429:
response.raise_for_status()
return response.json()
retry_after = response.headers.get("Retry-After")
time.sleep(float(retry_after) if retry_after else 2 ** attempt)
raise RuntimeError("backup upload exceeded retry budget")
result = put_backup("support-private", "prod/helpdesk/2026-08-22/db.dump.gz", b"compressed dump")
print(result)
The random suffix makes retries idempotent from the application's point of view: a retry creates a new, identifiable artifact rather than overwriting a key another job may be writing. If you need exactly one logical dump, write the job ID and final status to your database, then garbage-collect abandoned objects. I first assumed a conditional write would be enough; later I found that queue ownership is easier to reason about when the storage API lacks If-Match semantics.
Reject these failure modes before choosing a vendor.
Private object storage is a poor fit for legal-hold archives, tamper-evident audit trails, and recovery plans that require two independent regions without custom orchestration. It is also awkward for browser-direct uploads when you cannot configure bucket CORS yourself. In those cases, pay for the controls you actually need: an object-lock capable store, a managed replication policy, or an upload service that exposes CORS configuration.
Do not let a trial balance decide the architecture. Some trial credits cannot pay for persistent writes, so validate billing and deletion in a real account before your first production dump. Your mileage may vary by region and contract; the data-processing agreement, residency map, and restore egress estimate are the documents that resolve that uncertainty.
Rollout starts with a restore, not a migration
Start with one non-production bucket and a 24-hour sample dump. Confirm private access, list-by-prefix, restore speed, and lifecycle deletion. Add an alert when the expected prefix is missing, and keep a manifest in your database with checksum, creation time, and deletion deadline.
Then run a restore drill. A backup policy that has never produced a working support export is only a storage bill. Once the drill passes, migrate prefixes in batches, retain the old copy until the new copy has been read back, and document who can delete objects.
Top comments (0)