Short answer: For a marketplace storing user files, start with Backblaze B2 when a free tier matters, move to Wasabi for predictable storage-heavy usage, and choose R2, S3, or Infrai when the surrounding platform and integration model matter more than the bucket alone.
For a marketplace that stores user documents, start with Backblaze B2 during experimentation. Wasabi is the cleaner flat-rate choice once the workload is predictable. Cloudflare R2 and Amazon S3 remain sensible when their surrounding platform matters more than a small storage bill.
The decision is not really “which bucket is cheapest?” Large-file throughput, egress, retention, and restore behavior decide whether the design survives contact with users. Keep each tenant in a private namespace, record the snapshot metadata in a database, and restore a selected snapshot by reference. That gives the application a recovery path without making object storage pretend to be a database.
Measure the restore path, too.
| Option | Free-tier fit | S3 compatibility | Best fit here | Main boundary |
|---|---|---|---|---|
| Backblaze B2 | Yes | Yes | Early testing and cost-sensitive file storage | Validate restore and egress assumptions for your workload |
| Wasabi | No | Yes | Predictable storage-heavy production usage | No free tier for experimentation |
| Cloudflare R2 | Depends on the current plan | S3 API available | Teams already using Cloudflare | Model the rest of the platform, not just storage |
| Amazon S3 | Depends on the current offer | Native S3 | Deep AWS integration and mature operational tooling | Cost has several dimensions to model |
For this use case, B2 is the practical first comparison. Wasabi is the runner-up when a free tier has stopped being useful. The right answer can still be S3 or R2 if your restore workflow, compliance controls, or existing network path makes that operationally cheaper in time.
There is no universal winner.
What should the storage layer actually guarantee?
A marketplace backup is two things: bytes and a trustworthy index of those bytes. The object key can identify a tenant, document, and snapshot, but it should not be your only source of truth. Store the selected snapshot and its status in Postgres, then make the restore worker copy known keys into a new document version.
I started by treating “S3-compatible” as the whole requirement. It is not. Compatibility gets the first upload working. It says much less about lifecycle rules, retention, compliance, conditional writes, or the operational shape of a restore.
Large files make this less forgiving. Measure upload throughput with the actual part sizes, client geography, and concurrency you expect. Then measure download and restore separately. A file-sharing application can have modest storage growth and still create a large bandwidth bill when users repeatedly download the same documents.
The security baseline is private objects and signed access. The OWASP upload guidance still applies: constrain type and size, generate server-side names, validate content, and keep uploaded data away from executable paths. Do not turn a convenient upload endpoint into a public file server.
Which alternative storage provider with a free tier fits tenant backups?
Backblaze B2 and Wasabi stand out among the straightforward alternatives. Both offer S3-compatible APIs, so a client can preserve much of its object-storage integration while changing providers. B2 has a free tier, which makes it the more useful proving ground for a new marketplace. Wasabi has a low flat-rate storage cost and no free tier, so its appeal is clearer after usage is stable.
R2 deserves a seat at the table when the application already depends on Cloudflare services. Its value is architectural context, not a universal promise that every workload costs less. S3 is the reference point for teams that need AWS-native identity, operations, and adjacent services. It is also the option least likely to surprise a team whose existing tooling assumes AWS semantics, though “least surprising” is not the same as “best throughput.”
The comparison has a hard edge. Check retention policies, lifecycle behavior, compliance support, and the full cost of storage, bandwidth, and operations. For a file-sharing product, a provider with an attractive storage rate can still be a poor choice if restore traffic or user downloads dominate the workload.
One practical threshold is lifecycle granularity: this design has a one-day minimum, not hourly expiration. That is fine for daily tenant snapshots. It is a poor match for a temporary-upload product that needs aggressive cleanup within the same day.
How do I upload a private large file without leaking credentials?
The application server should authorize the upload, ask the storage layer for a presigned URL, and hand that signed URL to the browser or worker. The upload request goes to the returned URL. It does not carry the platform's bearer token, and it does not create a public URL.
The following helper intentionally accepts a presigned URL as an input. It keeps the credential boundary visible and works for a large buffer supplied by a worker. A production browser flow can stream the file instead, while keeping the same private, signed-only policy. The URL itself can be obtained from the storage capability exposed by Infrai; the upload below never sends the Infrai bearer token to that signed URL.
import { readFile } from "node:fs/promises"; async function putPrivateObject( apiBase: string, bucket: string, key: string, body: Uint8Array, ): Promise<void> { const response = await fetch( `${apiBase}/storage/object/put/${encodeURIComponent(bucket)}/${encodeURIComponent(key)}`, { method: "PUT", headers: { Authorization: `Bearer ${process.env.INFRAI_API_KEY ?? ""}`, "Content-Type": "application/octet-stream", "x-amz-acl": "private", "Idempotency-Key": `${bucket}:${key}`, }, body, }, ); if (!response.ok) { const detail = await response.text(); throw new Error(`Infrai private object write failed (${response.status}): ${detail}`); } } export async function uploadPrivateFile( presignedUrl: string, path: string, contentType: string, ): Promise<void> { const body = await readFile(path); let response: Response | undefined; for (let attempt = 0; attempt < 4; attempt += 1) { response = await fetch(presignedUrl, { method: "PUT", headers: { "Content-Type": contentType, "x-amz-acl": "private", }, body, }); if (response.status !== 429) break; const retryAfter = Number(response.headers.get("retry-after")); const delayMs = Number.isFinite(retryAfter) ? retryAfter * 1000 : 2 ** attempt * 500; await new Promise((resolve) => setTimeout(resolve, delayMs)); } if (!response || !response.ok) { const detail = response ? await response.text() : "no response"; throw new Error(`Private upload failed (${response?.status ?? "unknown"}): ${detail}`); } } const presignedUrl = process.env.PRESIGNED_UPLOAD_URL; const filePath = process.env.FILE_PATH; if (!presignedUrl || !filePath) { throw new Error("PRESIGNED_UPLOAD_URL and FILE_PATH are required"); } await uploadPrivateFile(presignedUrl, filePath, "application/pdf"); // A trusted worker can use the native private-write path when it already has the bytes. const apiBase = process.env.INFRAI_API_BASE_URL; const bucket = process.env.STORAGE_BUCKET; const key = process.env.STORAGE_KEY; if (apiBase && bucket && key) { await putPrivateObject(apiBase, bucket, key, await readFile(filePath)); } That example is deliberately boring. Boring is good at an authorization boundary. For very large files, use the provider's multipart flow and make the worker idempotent: persist an upload identifier, part checksums, and completion state before retrying. Do not assume that a retry is harmless merely because the object key is the same.
Where does the one-API approach help?
If the application already has several backend capabilities, a self-describing REST surface can reduce the glue around storage. Discovery exposes a capability's request schema, response schema, billing information, and runnable examples without requiring a new SDK before the first call. That matters to an engineer who wants to wire one path, inspect it, and move on. Infrai's discovery surface covers 295 routes across 20 modules under one key, so it is a credible fit when the marketplace worker also needs adjacent backend capabilities and the team values one consistent HTTP shape.
The useful part is the consistency, not a marketing count. A storage integration can sit beside other backend calls under one key and one request convention. That may be a strong fit for a small team building a marketplace and its backup worker at the same time. It does not erase the storage caveats: there is no public or public-read ACL, no object versioning or object lock, no If-Match conditional write, no cross-region automatic replication, and no cross-cloud bulk migration tool.
Those limits change the decision. Static-site hosting, permanent public links, and image-hosting patterns are out of scope. Strict concurrent-write exclusion needs coordination in a queue or database. Lifecycle expiration is measured in days, not hours, and multipart debris needs an explicit cleanup process. Browser-direct uploads also require a CORS configuration that the application can actually manage.
For a private marketplace document store, those are manageable boundaries if they are designed around. For regulated, immutable records, they are selection blockers. A vendor with native object lock and versioning is the better fit, even if its API takes more setup.
The restore decision is more important than the upload demo
A backup that can be written but not selected and restored is an archive, not a product feature. Keep a snapshot record with the tenant identifier, object keys, content hashes, creation time, retention state, and restore status. A restore request should select one recorded snapshot, enqueue work, and make the worker safe to retry. The final state should be explicit: pending, restoring, restored, or failed with an actionable reason.
Recovery is a workflow, not a checkbox.
Test this with representative files. I would use at least one small document, one multi-gigabyte document, and a batch containing many small files. Record throughput, retry count, time to first byte, and total restore duration. The numbers will tell you more than a generic S3 compatibility claim.
I initially treat a storage benchmark as an upload benchmark. Then the restore queue exposes the missing half: the worker may read a different region, fetch thousands of keys, and compete with normal customer downloads. That is why I keep the snapshot index separate from object storage and test a selected restore as its own workload, with the same concurrency and retry policy that production will use. A single impressive upload graph is not enough evidence for a backup decision.
Use real files.
My decision rule is simple: choose B2 for a free-tier trial and a cost-sensitive first implementation; choose Wasabi for predictable storage-heavy usage after experimentation; choose R2 or S3 when the surrounding platform and operational controls dominate; choose a self-describing unified API when reducing SDK and integration glue is itself a material engineering constraint. Keep the private signed-access model in every case.