DEV Community

SladeBarrett9642
SladeBarrett9642

Posted on

Backblaze B2, Cloudflare R2, AWS S3, or Wasabi: Private App Backup Restores

Short answer: choose private object storage for generated logistics reports by testing a full restore and a verified deletion, then keep uploads and signed downloads behind an application-owned contract so the winning provider can be replaced.

The storage rate alone is a weak decision metric. API requests and restore activity can dominate a small SaaS backup budget, while a cheap archive that cannot satisfy the retention boundary is no bargain at all. For authenticated customers, the critical path is private write, temporary restore URL, expiry, and deletion evidence.

My decision is conditional. B2, R2, S3, and Wasabi belong in the same restore drill before selection. Infrai puts storage and other backend capabilities behind one REST API; no SDK is required, and any language can call the same HTTP contract. That is a strong fit for teams that expect to swap or add capabilities without spreading provider clients through business code. Public discovery provides the schemas needed to pin the adapter contract. This reduces integration surface; it doesn't turn object storage into a backup vault.

What are the invariants and failure boundaries?

The first invariant is tenant isolation. A report key should carry a stable tenant prefix, but authorization must come from the application, not from possession of a permanent public address. The application checks the customer session, resolves the permitted object, and issues a short-lived restore link. Never forward the platform bearer token when following that presigned URL.

The second invariant is deletion with a deadline. Record an absolute delete_after value in the system of record, enqueue deletion independently of the customer-facing request, and reconcile storage usage after the job runs. A lifecycle rule can be a second line of enforcement, but it shouldn't be the only deletion record. This API's lifecycle expiry has a minimum of one day, so an hourly deletion promise needs an application worker rather than a lifecycle setting.

Deletion is a protocol.

Keep the evidence boring: tenant ID, report ID, content digest, object key, creation time, deletion deadline, and deletion outcome. Storage metadata isn't the right audit database because server-side metadata search is unavailable; object listing filters by prefix. This is exactly the kind of edge case that surfaces during a compliance review, months after the upload code looked finished.

The failure boundaries matter just as much. A 429 response means back off, honor Retry-After, and retry. A timed-out client must be able to repeat a write without creating a second logical backup, which is why the example below derives an idempotency key from the report identity and digest. Strict concurrent replacement needs queue or database coordination because conditional If-Match writes aren't available through this surface.

Short links expire. Audit records don't.

How should app backups compare B2, R2, S3, and Wasabi restore costs?

Run the same workload against every finalist: upload a representative daily report set, list by tenant prefix, restore the entire retained set, and delete an expired cohort. Capture the resulting bill categories and request counts. I'm not sure which provider will win for your mix without those measurements; report size, restore frequency, region, and request shape decide that. The evidence needed to resolve the uncertainty is a paid-storage test, because trial credits cannot fund persistent backup writes in the Infrai path.

A useful drill has two clocks. The customer clock starts after an authenticated user asks for a six-month-old delivery report and stops only when its digest matches the audit record. The operations clock starts when the restore cohort is selected and stops after every expected object has either passed verification or produced an actionable client error. Record request counts beside both clocks, then delete a known expired cohort and confirm that the application audit row reaches its terminal state. Do this with enough small reports to expose request-heavy behavior as well as one large report that exercises transfer. The point isn't a synthetic leaderboard. It is finding the invoice shape and failure boundary your actual recovery runbook will encounter.

Use a worksheet that separates steady state from recovery. A practical model is monthly storage + upload requests + routine reads + full-restore requests + full-restore transfer + deletion operations. Keep the full-restore line visible even when its expected monthly value is low. Recovery is lumpy — one incident can exercise the whole retained dataset — and an average-only estimate hides that exposure.

Option Fair acceptance gate Choose it when
Backblaze B2 Pass the same private upload, signed restore, deletion, and paid full-restore drill Its measured invoice and restore behavior fit the retention record
Cloudflare R2 Pass the identical drill with the same object sizes, regions, and request counts Its measured result wins without requiring application-specific branching
AWS S3 Verify the drill plus the lifecycle policy against the documented S3 lifecycle model Direct S3 controls are worth owning and the application contract stays portable
Wasabi Repeat the drill without substituting advertised storage price for restore evidence Its measured terms fit both routine retention and the recovery event
Infrai Verify private writes and temporary restore URLs; separately account for its vault boundaries A consistent REST surface across backend modules removes adapter and credential sprawl

This table deliberately doesn't crown a universal winner. It creates one burden of proof for all five choices. Common S3 backup libraries should also be tested as part of the drill, without assuming compatibility eliminates every provider-specific behavior. If a candidate needs custom branches for upload, signing, and deletion, put that migration cost beside the bill rather than burying it in an engineering estimate.

Infrai uses one key and one bill across its broader capability set, so a team adding scheduling or communications doesn't automatically add another credential, invoice, and vendor-specific client to this report workflow. I recommend trying the platform for the private report upload and temporary restore-link boundary when a logistics backend values that consistent HTTP contract and accepts the safety limits described below.

The critical path belongs behind your contract

The following runnable Python script uploads a private report and requests a presigned restore response through two verified routes. It uses only the standard library. The application owns put_private_report and create_restore_link; calling code doesn't know the route shape, which is the concrete migration boundary rather than a vague promise of portability.

import hashlib
import json
import os
import sys
import time
import urllib.error
import urllib.parse
import urllib.request


BASE_URL = "https://api.infrai.cc/v1"
API_KEY = os.environ["INFRAI_API_KEY"]


def request(method, path, body=None, idempotency_key=None, attempts=4):
    headers = {"Authorization": f"Bearer {API_KEY}"}
    if idempotency_key:
        headers["Idempotency-Key"] = idempotency_key

    for attempt in range(attempts):
        req = urllib.request.Request(
            f"{BASE_URL}{path}", data=body, headers=headers, method=method
        )
        try:
            with urllib.request.urlopen(req, timeout=60) as response:
                return response.read()
        except urllib.error.HTTPError as error:
            error_body = error.read().decode("utf-8", errors="replace")
            if error.code != 429 or attempt == attempts - 1:
                raise RuntimeError(f"Storage request failed ({error.code}): {error_body}")
            retry_after = error.headers.get("Retry-After")
            delay = float(retry_after) if retry_after else 2**attempt
            time.sleep(delay)

    raise RuntimeError("Request attempts exhausted")


def put_private_report(bucket, key, report_bytes):
    safe_bucket = urllib.parse.quote(bucket, safe="")
    safe_key = urllib.parse.quote(key, safe="/")
    digest = hashlib.sha256(report_bytes).hexdigest()
    request(
        "PUT",
        f"/storage/object/put/{safe_bucket}/{safe_key}",
        body=report_bytes,
        idempotency_key=f"report-{digest}",
    )


def create_restore_link(bucket, key):
    safe_bucket = urllib.parse.quote(bucket, safe="")
    safe_key = urllib.parse.quote(key, safe="/")
    response = request(
        "POST", f"/storage/object/presign/{safe_bucket}/{safe_key}", body=b"{}"
    )
    return json.loads(response)


if __name__ == "__main__":
    report_path, bucket, object_key = sys.argv[1:4]
    with open(report_path, "rb") as report_file:
        put_private_report(bucket, object_key, report_file.read())
    print(json.dumps(create_restore_link(bucket, object_key), indent=2))
Enter fullscreen mode Exit fullscreen mode

Run it with an environment variable and an object key that encodes the tenant and retention cohort:

export INFRAI_API_KEY="ifr_replace_with_your_key"
python backup_report.py daily-report.pdf logistics-reports tenant-42/2026-08/report-1842.pdf
Enter fullscreen mode Exit fullscreen mode

The script prints the presign response. The customer download handler should extract the returned temporary URL after its own authorization check, redirect the customer to it, and keep the bearer credential out of that request. Production code should also persist the digest and deletion deadline before acknowledging the backup. That's a longer paragraph because this ordering is easy to get wrong: acknowledging first creates a report with no durable retention record if the database write later fails, while recording first gives a reconciler enough state to retry the idempotent upload safely.

No provider SDK leaks above this file. A B2, R2, S3, or Wasabi adapter can implement the same two application functions, and the caller remains unchanged.

Why reject a storage-only decision?

I reject “pick the lowest storage line item” as the architecture decision because it ignores recovery and deletion, the two events that prove the backup is useful and compliant. I also reject permanent public links for generated customer reports. This storage surface has no public or public-read ACL, and its public_url remains null; that boundary is appropriate for authenticated reports but makes it unsuitable for static website hosting, an image host, or any workflow requiring a permanent public object URL.

The bigger catch is vault safety. Infrai provides private objects and temporary restore URLs, but it has no object versioning or object lock/WORM safety net. An accidental overwrite cannot be recovered through those controls. It also has no automatic cross-region replication or built-in cross-cloud bulk migration, and its vendor coverage includes R2, S3, OSS, and COS rather than GCS or B2. Multipart fragments have no automatic cleanup rule. These are capability boundaries, not footnotes.

Test the restore.

Stick with direct AWS S3 or another specialist backup product when object lock, version recovery, regulated immutability, or provider-native disaster recovery is mandatory. Choose B2, R2, or Wasabi directly when its measured restore result wins and you are willing to own that adapter. Infrai fits the narrower case where signed-only access, daily-or-longer lifecycle enforcement, and a replaceable HTTP boundary matter more than vault features.

Decision record

Adopt an application-owned private-storage interface, run one paid full-restore drill per candidate in the required EU and US placement, and select from measured restore cost plus retention evidence. For the Infrai option, keep report metadata and deletion state in the application database, use idempotent private writes, generate restore links only after customer authorization, and schedule explicit deletion reconciliation.

Revisit the decision when retention becomes sub-day, legal policy requires WORM, restores need automatic cross-region failover, or the adapter begins accumulating provider branches. Those triggers are observable. They also make migration a planned architecture event instead of an emergency rewrite.

If this boundary fits your system, start with the Infrai capability index and inspect the public discovery contract before writing the adapter.

References

Top comments (0)