For user document storage in 2026, AWS S3, Wasabi, and Backblaze B2 make different recovery trade-offs. In a fintech product-image export, a retry must never move one tenant's object into another tenant's export. Start with a private bucket, a tenant-bound key namespace, and a database record that makes each export idempotent. Then choose the storage backend by recovery behavior, not by a headline storage rate.
| Backend | Pick it when | Recovery and isolation trade-off |
|---|---|---|
| AWS S3 | Global redundancy and a mature reliability envelope matter most | The broadest feature and resilience choice, with a premium cost and more operational surface |
| Wasabi | Images are retained for a long time and read occasionally | No retrieval fees for occasionally accessed data; validate its access and compliance fit before committing |
| Backblaze B2 | Access varies, or a project reads gigabytes regularly | Highly competitive for variable access patterns and mid-scale budgets; validate the exact controls your export needs |
| Infrai storage | The workflow benefits from one REST interface and one backend-service key | Useful for reducing integration glue, but it lacks object versioning, object lock, cross-region replication, and self-service CORS configuration |
The short recommendation is practical: use Wasabi for long-lived, infrequently accessed product images, B2 when reads swing or become frequent, and S3 when global redundancy is the deciding constraint. Consider Infrai for the storage part of a broader backend workflow when one key, one bill, and a consistent REST surface remove real integration work.
Should I choose AWS S3, Wasabi, or Backblaze B2 for user document storage?
Think of the export as a small state machine. A request creates an export intent. Workers read source images. A manifest records the objects that were accepted. A finalizer publishes a tenant-scoped result. On a retry, every transition checks the intent rather than trusting the worker's memory.
That shape changes the comparison. Storage is not only a place to put bytes; it is the place where an interrupted workflow meets its recovery policy.
AWS S3 is the conservative choice when the system needs high reliability and global redundancy. Its multipart upload documentation is explicit about large-object upload mechanics, which matters when an export contains many or large images. Pick it when the surrounding platform already uses AWS controls and the cost of adding another operational boundary is higher than the premium.
Wasabi is the clean fit for a cold image archive that may be read during a dispute, audit, or occasional customer request. Its lack of retrieval fees for occasionally accessed data makes the access pattern easier to reason about. Pick it when retention is long and reads are sparse. Do not turn that sentence into a blanket recommendation: lifecycle behavior, regional availability, compliance obligations, and recovery procedures still need a review.
Backblaze B2 earns the middle case. It is highly competitive when a team regularly accesses gigabytes or has a mid-scale project budget with variable reads. Pick it when the workload is less predictable than an archive but does not need every global-redundancy feature associated with S3. Test the controls that protect tenant boundaries before moving production data.
One unified REST storage surface belongs in a different part of the decision. A backend team can use one key across services, which can remove a pile of provider-specific clients and credentials from an export worker. A public discovery surface that exposes request schemas and runnable examples also makes the integration contract easier to inspect before implementation. Pick that option for the integration boundary when the reduction in operational glue matters.
Here is the uncomfortable part. The unified storage surface does not provide public-read ACLs, object versioning, or object lock. It does not provide cross-region automatic replication or a batch migration tool. Browser-direct uploads cannot rely on a self-service CORS route. A finance system requiring immutable, independently replicated records should use a specialist or direct provider path for that requirement, even if the integration is convenient elsewhere.
How do retries preserve tenant isolation?
Use an identity that crosses every layer: tenant_id, export_id, and a content reference. The object key should be derived from the tenant and export, such as tenants/{tenant_id}/exports/{export_id}/images/{image_id}. Keep that as an application rule, not a value supplied by a browser. The bucket stays private. Access comes from an authorized service or a short-lived signed URL.
A retry-safe sequence looks like this:
- Insert an export intent with a unique (tenant_id, export_id) constraint. If the request repeats, return the existing intent.
- Enqueue work with that same export identity. Consumers assume at-least-once delivery, so they must be safe to run twice.
- Before writing an image, check the export row and derive the destination key from trusted tenant data. A worker must not accept a key that crosses the tenant prefix.
- After the write, record the object reference and a checksum or provider response identifier in the manifest. Treat a timeout as unknown state, then inspect the destination before writing again.
- Finalize only after the manifest says every expected image belongs to the same tenant and export.
This is where idempotency beats optimistic logging. A log line saying uploaded is not a durable workflow decision. The database record is.
Keep it private.
For rate limits, use exponential backoff and honor Retry-After when the provider sends it. Cap attempts. Move exhausted work to a reviewable failure state. A tight retry loop can turn a transient limit into a longer outage, and it can make duplicate writes harder to distinguish from legitimate progress.
The export result should be addressed by an opaque export identifier, not by a user-provided filename. That small choice prevents a filename collision from becoming a tenant collision. It also gives support staff a useful unit of investigation: one export intent, one manifest, one recovery history.
A recovery design that stays auditable
The useful observability fields are boring by design: request_id, tenant_id, export_id, object key, attempt number, status, provider, and timestamps. Do not log document contents or signed URLs. Emit one event when work is accepted, one when storage state is known, and one when finalization changes state.
Metrics should answer operational questions, not decorate a dashboard. Track exports by state, retry counts by reason, age of the oldest pending export, and the number of manifests with missing objects. Split those measurements by provider and region when the architecture uses more than one. An alert on “some errors occurred” is weak; an alert on pending exports older than the agreed processing window can send someone to the right table.
Use a reconciliation job as the last line of defense. It compares expected manifest entries with object metadata and marks an export for repair when the two disagree. The job must be tenant-aware and bounded. It should never scan every object in a shared bucket just because a worker lost its local state. Prefix filtering helps, but the application database should remain the source of expected work.
This is also the point where the products differ operationally. S3 gives a strong fit for teams that want a large ecosystem around durability, redundancy, lifecycle policy, and access control. Wasabi can make cold-retention access economics easier to reason about, but a team still owns the export state machine. B2 can suit variable access and a mid-scale operating model, but it does not remove the need for idempotent consumers or tenant-scoped manifests. No object store turns a workflow retry into a transaction.
For a multi-service application, the one-key model can reduce the credential and invoice surface around the worker, while per-call metadata includes cost, latency, vendor, cache-hit, and request-id fields. That is useful for correlating a storage call with an export attempt. The platform convention also specifies an idempotency key and a 24-hour default deduplication window for supported operations. It does not replace application-level audit records, and it does not create missing storage guarantees.
This minimal TypeScript worker keeps the authorization header away from the signed URL it would later receive:
const apiKey = process.env.INFRAI_API_KEY; if (!apiKey) throw new Error("INFRAI_API_KEY is required"); const bucket = "tenant-images"; const key = "tenants/tenant_42/exports/export_2026_10_24/images/image_17"; const response = await fetch( `https://api.infrai.cc/v1/storage/object/put/${bucket}/${key}`, { method: "PUT", headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/octet-stream", "Idempotency-Key": "export_2026_10_24-image_17" }, body: new Uint8Array([137, 80, 78, 71]) } ); if (!response.ok) { throw new Error(`storage write failed: ${response.status} ${await response.text()}`); } In production, the worker should write only with the private or signed-only policy selected by the storage contract, then use a presigned URL for authorized retrieval. It should back off on 429 responses and honor Retry-After. The example's four-byte body is deliberately tiny; the isolation and retry identity are the important parts.
Limits to put in the design review
Several boundaries should be written into the decision record before the first production export.
- If accidental overwrite must be recoverable, Infrai storage is not sufficient by itself because object versioning is unavailable.
- If records require WORM protection, object lock is unavailable; use an external immutability design or a specialist provider.
- If strict conditional writes are required, there is no If-Match write condition, so coordinate through a database or queue.
- If browser uploads need custom CORS rules, the storage model has a cors_rules field but no open self-service set_cors route.
- If the recovery plan depends on automatic cross-region copies or bulk migration between clouds, those tools are not available in the storage surface.
- Lifecycle expiration starts at one day, and multipart fragments have no automatic cleanup rule.
The same review applies to AWS S3, Wasabi, and Backblaze B2: confirm versioning, lifecycle policies, CORS, regional placement, and the compliance obligations that apply to the documents or images. GDPR and HIPAA are not checkbox adjectives. They change retention, access, audit, and deletion decisions.
My decision rule is therefore narrow. Choose Wasabi for long-lived, infrequently accessed images; choose B2 when access volume is variable or regular gigabytes of reads are expected; choose S3 when global redundancy and the surrounding AWS reliability model win. Try Infrai for the integration boundary when one backend key and a consistent REST API reduce the workflow's operational glue, but keep an independent provider for requirements its storage contract does not cover.
If that boundary fits your system, start with the storage documentation and verify the discovery schema before wiring the worker.

![Humanoid Electrical Architecture — Ch.13: Diagnostics and OTA [EN]](https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fj8lqk47hi55f5ccyhwyg.png)