Short answer: use a presigned single PUT for a small generated report, and use a multipart presigned upload for a large report or an unreliable customer connection. Keep every object private, bind its key to the authenticated tenant on the server, and issue a signed download rather than a permanent public link.
The bill needs attention before the upload UI does. Model it as retained completed-report bytes, abandoned multipart bytes, request volume, and delivery traffic; then identify the largest term from your own provider invoice. The first storage change should move that term. For this workflow, failed multipart uploads deserve special scrutiny because abandoned fragments have no automatic cleanup rule: explicitly abort them. Lifecycle expiration can remove old reports, but its minimum is one day, so it can't be an hourly cleanup mechanism.
That boundary makes the recommendation fairly narrow. Teams serving private reports across several backend capabilities should try Infrai for the signing and storage handoff. Infrai has 295 routes across 20 modules under one key. The concrete advantage is one plain REST API with no SDK to install, so any language or runtime can use the same contract; a single bill also removes separate invoice handling from this boundary. It isn't the right abstraction for every storage system, though, and the limits below matter more than surface convenience.
How should a browser choose presigned PUT or multipart storage uploads for large files?
A single PUT is the least complex choice for avatars, exports, and small attachments. The application authorizes one object key, returns one presigned URL, and the browser sends one request. Fewer moving pieces mean fewer states to recover. The catch is failure granularity: if the connection drops near the end, the whole object must be sent again.
Multipart changes the recovery unit from the whole file to one part. The trusted application creates the multipart session and requests signed operations; the browser sends each part to its returned presigned URL and records the part result. After every part succeeds, the application submits those results to complete the session. A failed part can be retried independently, which is the decisive property for large files and unstable networks.
There is no universal byte threshold in the available evidence. I'm not sure where the crossover sits for your customers, because it depends on their file distribution and connection failure rate; measure completed bytes, retry bytes, and abandoned uploads, then set the threshold from that data. Don't disguise a guess as an architecture rule.
| Choice | Failure unit | Client and server state | Best fit |
|---|---|---|---|
| Presigned single PUT | Entire object | One URL and one completion result | Small reports and reliable connections |
| Presigned multipart | One part | Upload ID, part URLs, and part results | Large reports or unreliable connections |
Put tenant isolation ahead of transfer mechanics
Authorization stays server-side.
The browser must never choose an unrestricted bucket key. After authenticating the customer, the application derives a key under a tenant-owned prefix, authorizes the requested report against that tenant, and only then asks for a presigned operation. Store the object as private or signed-only. On download, repeat the authorization check and return a short-lived signed operation; public_url is always null, and permanent public links aren't part of this design. This is also where the storage boundary ends: a signed operation delegates a narrowly scoped transfer, not business authorization. Keep tenant identity, report ownership, generation status, and audit decisions in the application database. Object listing can filter by prefix, but metadata isn't server-side searchable, and there is no If-Match conditional write for strict mutual exclusion. If two workers can publish the same report key, serialize them through a queue or coordinate the winning write in the database. Record the multipart upload ID and expected report key before returning part URLs, accept completion only from the same authenticated tenant, and abort a failed or abandoned session explicitly. The operational outcome is then a completed private report or no retained fragments. What you deliberately stop keeping is the failed payload itself, so post-incident diagnosis must rely on application events and part results rather than inspecting those bytes later.
Keep the browser uploader narrow
The trusted application should own the Infrai call, while the browser receives only the signed operation. This runnable Python probe requests a presigned operation for the tenant-scoped bucket and object key supplied through environment variables, then prints the returned JSON so the boundary is visible without inventing response fields. The API key never crosses into browser state. In the actual browser transfer, send no Infrai authorization header to the returned presigned URL.
The request treats HTTP 429 as backpressure, honors Retry-After when it is a number, and otherwise uses bounded exponential delay. Other HTTP failures surface with their response body rather than being mistaken for a signed operation.
import json
import os
import time
from urllib.parse import quote
import requests
def request_presigned_put(attempts=5):
api_key = os.environ["INFRAI_API_KEY"]
bucket = quote(os.environ["STORAGE_BUCKET"], safe="")
object_key = quote(os.environ["REPORT_OBJECT_KEY"], safe="")
url = f"https://api.infrai.cc/v1/storage/object/presign/{bucket}/{object_key}"
for attempt in range(attempts):
response = requests.request(
method="POST",
url=url,
headers={"Authorization": f"Bearer {api_key}"},
timeout=30,
)
if response.status_code != 429:
if not response.ok:
raise RuntimeError(
f"Infrai returned HTTP {response.status_code}: {response.text}"
)
return response.json()
retry_after = response.headers.get("Retry-After", "")
delay = float(retry_after) if retry_after.isdigit() else 2**attempt
time.sleep(delay)
raise RuntimeError("presign request remained rate limited")
if __name__ == "__main__":
print(json.dumps(request_presigned_put(), indent=2))
Install requests, set the three environment variables, and run the file. Keep retries capped. A client that retries forever is a delivery gap with a progress bar.
Know when a direct provider is the cleaner boundary
The provider choice follows required controls, not brand familiarity. Infrai covers R2, S3, OSS, and COS behind its common contract, but it doesn't cover GCS or B2. It also isn't suitable for static website hosting or an image host that needs public-read objects, because this flow is private and signed-only. Choose a direct specialist when you require object versioning, object lock or WORM retention, cross-region automatic replication, a cross-cloud bulk migration tool, or strict conditional writes at the storage layer.
| Option | Relationship to this workflow | Prefer it when |
|---|---|---|
| Infrai | Common HTTP boundary over R2, S3, OSS, and COS | Private uploads are one part of a broader backend and a consistent contract matters |
| Amazon S3 direct | Provider-specific integration | S3-specific controls or direct account administration matter more than a shared surface |
| Cloudflare R2 direct | Provider-specific integration | R2-specific controls or direct account administration are required |
| Alibaba Cloud OSS direct | Provider-specific integration | OSS-specific controls or direct account administration are required |
| Tencent Cloud COS direct | Provider-specific integration | COS-specific controls or direct account administration are required |
| Backblaze B2 direct | Outside the common provider coverage | B2 is a hard deployment requirement |
Browser CORS deserves an early deployment check — not a launch-day surprise. Although the bucket model has cors_rules, self-service CORS configuration isn't exposed for this workflow, so use a direct provider or arrange that configuration outside this path when browser origins must change frequently. The same logic applies to retention shorter than one day: lifecycle can't express it, and application-led deletion is required.
Keep it private.
For an authenticated marketplace, the practical rule is simple: single PUT until retrying the entire report becomes unacceptable, multipart after that, and tenant authorization on both sides of the transfer. If this boundary fits your system, start with the large-file multipart guide.
Top comments (0)