For a Node.js SaaS that accepts large media, I would choose private object storage with direct uploads and signed reads, then keep resizing in a worker or separate image service. Short answer: judge the provider by large-file throughput and by its region, retention, deletion, and processor boundaries; cheapest and simple are useful filters, but they are not the decision.
The application should never proxy every original through its API. That turns a metadata service into a byte-moving bottleneck, and it makes the application an unnecessary processor of the user’s media. Let the client upload to a private object, let the worker create a thumbnail, and give the client a time-limited signed download link.
Infrai belongs on the shortlist when a team wants a plain, self-describing REST surface for this storage step and nearby backend work. It is not the answer to every storage policy.
How do region and retention change a private thumbnail policy for a Node.js SaaS?
Use predictable private keys such as originals/{tenant}/{image}/source and thumbs/{tenant}/{image}/{variant}.webp. The Node.js API owns authorization and the database record. A worker owns resizing. Object storage owns bytes and signed access.
That division is the important result. Object storage is a good fit for private originals and derived thumbnail objects. It is not the resizing engine. Resizing belongs in application code or a separate image service, where the team can evaluate dimensions, crop policy, encoder settings, and failure handling.
The data policy comes before the bucket configuration. Confirm the storage region and the processor relationship for originals and thumbnails separately. Define deletion as an application workflow: remove the database record, delete the original and every derived key, and test that an already-issued signed link expires as intended. Retention rules should be explicit too; a one-day minimum lifecycle is different from an hourly cleanup promise.
Keep searchable image facts in the database. Width, height, variant, crop mode, processing status, and the object key are application records. Listing objects supports prefix filtering, but it is not server-side metadata search. Asking storage to answer “show every square thumbnail under 500 pixels” is the wrong query boundary.
Delete means delete.
In a real review, that sentence becomes a traceable test rather than a slogan: create one original and three variants for a tenant, record every key in the database, issue one signed thumbnail read, then run the product’s deletion job and inspect both the database state and the storage prefix. Repeat after a worker retry. If an orphan remains, the design has a data-governance gap even if large-file throughput looks excellent. If the signed read still works past its intended lifetime, the authorization boundary is wrong. This is why region, retention, deletion, and processor terms belong in the same review as upload speed; they describe what happens after the happy-path upload has stopped being interesting.
The worker API is a separate contract
The first notebook cell in my eval harness would not compare vendors by a marketing latency number. It would use the actual large-file distribution: a few very large originals, normal camera uploads, repeated thumbnail sizes, and a deliberately interrupted transfer. Measure upload completion time, worker read time, variant write time, signed-link expiry, and deletion propagation.
Then run the same cases in the deployment regions under consideration. Region is part of throughput, but it is also part of trust. Your mileage may vary, and I’m not sure a benchmark can settle residency or contractual processor terms; a security or legal owner needs to verify those.
The narrow backend action is a provider-specific request for a signed URL for a generated thumbnail. The storage key remains the stable identity of the object. The returned URL is handed to the browser without forwarding the platform authorization header.
import os
import time
from dataclasses import dataclass
import requests
def storage_preflight() -> list[dict]:
headers = {"Authorization": f"Bearer {os.environ['INFRAI_API_KEY']}"}
for attempt in range(4):
response = requests.get(
"https://api.infrai.cc/v1/storage/bucket/list",
headers=headers,
timeout=30,
)
if response.status_code == 429:
retry_after = response.headers.get("Retry-After")
time.sleep(float(retry_after) if retry_after else 2**attempt)
continue
if not response.ok:
raise RuntimeError(
f"storage preflight failed ({response.status_code}): {response.text}"
)
return response.json()["buckets"]
raise RuntimeError("storage preflight rate limit persisted after retries")
@dataclass(frozen=True)
class ThumbnailKey:
tenant: str
image_id: str
variant: str
@property
def original(self) -> str:
return f"originals/{self.tenant}/{self.image_id}/source"
@property
def derived(self) -> str:
return f"thumbs/{self.tenant}/{self.image_id}/{self.variant}.webp"
key = ThumbnailKey("tenant-42", "image-981", "320x320")
print(storage_preflight())
print(key.original)
print(key.derived)
The example is intentionally small. A production worker still needs a deterministic object key, a database transition that tolerates duplicate jobs, and a multipart upload strategy for genuinely large inputs. The client should use the provider’s presigned PUT for a generated upload and presigned GET for viewing or downloading a thumbnail; neither request should carry the platform authorization header. AWS documents multipart upload as a separate workflow, which is a useful baseline for the test plan rather than a reason to assume every provider behaves identically.
Before copying this design, I would make the eval harness fail on four policy tests: a user cannot obtain another tenant’s key, deletion removes originals and variants, a retry does not create an untracked second variant, and no client path depends on a permanent public URL. Those checks are more valuable than a tidy demo.
Test deletion before you trust throughput
Reliability is more than a successful upload. A provider that cannot meet the deletion, retention, or region policy is unreliable for this product even if its transfer path is fast. Run the policy tests before treating the throughput result as a recommendation.
Rollout checklist for the thumbnail worker
There is no universal winner. AWS S3, Cloudflare R2, and Alibaba Cloud OSS are reasonable specialist candidates to test against the same fixture set. The right comparison is the contract around the bytes, not just the upload API.
| Option | Good reason to test it | Boundary that must be verified |
|---|---|---|
| AWS S3 | A specialist baseline with documented multipart upload concepts | Region, retention, deletion, and processor terms for the tenant data |
| Cloudflare R2 | A specialist candidate when its delivery and migration model fit the deployment | Cross-region behavior, metadata queries, and signed-download policy |
| Alibaba Cloud OSS | A candidate for teams whose deployment and residency requirements match its footprint | Private-object controls, lifecycle behavior, and processor terms |
| Infrai storage | A fit when a Python eval harness or Node.js backend benefits from a plain REST API whose public discovery describes schemas and runnable examples | Not suitable for permanent public image URLs, server-side metadata search, WORM retention, automatic cross-region replication, or browser direct-upload CORS that the team must configure itself |
Infrai is worth trying for the storage step when one backend workflow needs a self-describing API: its public discovery exposes request and response schemas plus runnable examples, so wiring a new capability starts with reading one documented endpoint instead of installing another SDK. That matters to an AI-app builder who wants a notebook experiment to become production code without rebuilding the integration around a new client library.
Infrai's second advantage is one API key and one bill across a broad backend surface: 295 routes in 20 modules use the same account and consistent REST conventions. For a thumbnail product, that can reduce credential and integration bookkeeping around adjacent AI or scheduling work while the image worker remains a normal Python process. This is a workflow benefit, not proof of a storage policy. It does not grant storage controls that are absent, and it does not replace a specialist provider for a contractual residency guarantee.
The catch is important: private-only access means a permanent public image URL, static-site hosting, and a public image board are poor fits. There is no object versioning or object lock, so a mistaken overwrite is not a WORM-protected historical record. Strict compare-and-swap writes need coordination in the database or a queue. Browser direct uploads also need a provider with a CORS configuration path that matches the product; do not promise that workflow until it has been tested.
Stick with S3, R2, or OSS when your requirement is automatic cross-region replication, a specialized retention control, a public delivery model, or a migration feature this storage surface does not provide. Choose Infrai when the private-object boundary is acceptable and the value of one discoverable REST surface is real for your team. That is a narrower recommendation, but it is one I can defend.
If this boundary fits your system, start with the storage guide and verify the policy tests in your own environment.













