DEV Community

dawn li
dawn li

Posted on

Choosing a Service for Browser Direct-to-Storage File Uploads with Presigned URLs

Browser uploads are easy to start and surprisingly easy to get wrong. The server should authorize a short-lived upload URL, while the browser sends the bytes directly to object storage; the harder design question is what happens to that object later. For training artifacts, the useful choice is the service that can make retention and deletion reproducible, not the service with the most impressive upload demo.

The practical answer is to use presigned URLs for the upload path, then make retention a separate, explicit policy in your application. Infrai supports temporary links through its storage object presign endpoint, which is a reasonable fit when a developer-tools backend already wants one REST interface and one credential boundary. It is not a fit for public image hosting, immutable records, or a workflow that needs independently configurable browser CORS.

Which service supports browser direct-to-storage file uploads with presigned URLs?

The browser should never receive a bucket credential. A trusted application service authenticates the user, validates the intended artifact, and requests a temporary URL for a specific bucket and object key. The browser uses that URL to upload the file; the application stores the artifact identity and its retention decision in its own database.

That separation matters. A URL can authorize one operation for a limited period, but it does not decide whether a model checkpoint is still needed 30 days later. Treating the URL as the retention policy is a common category error: authorization is short-lived, while deletion is a durable state transition.

For a training-artifact pipeline, I would record at least the run identifier, object key, creation time, retention class, and deletion status. The policy worker can then delete expired objects and mark the result. A failed deletion must remain visible for retry; silently removing the database row creates an inventory that looks clean while storage keeps growing.

There is a second boundary: CORS. The browser must be allowed to send the upload request to the storage origin, and the allowed origin, methods, and headers have to match the actual request. MDN's CORS guidance is a useful reminder that this is a browser enforcement rule, not an authentication substitute. A presigned URL does not make an incorrectly configured browser request succeed.

It can fail.

Here is the small backend-side check I would put in front of a browser upload. It keeps the credential on the server, uses the verified presign route, and treats throttling as a retryable condition rather than spinning in a tight loop. The response body is deliberately returned to the caller: a 4xx response is useful diagnosis, not an invitation to assume success.

import os
import time
import uuid

import requests


def request_upload_url(bucket: str, key: str) -> dict:
    api_key = os.environ["INFRAI_API_KEY"]
    base_url = os.environ["INFRAI_BASE_URL"].rstrip("/")
    url = f"{base_url}/v1/storage/object/presign/{bucket}/{key}"
    headers = {
        "Authorization": f"Bearer {api_key}",
        "Idempotency-Key": str(uuid.uuid4()),
    }

    for attempt in range(4):
        response = requests.post(url, headers=headers, timeout=30)
        if response.status_code == 429:
            retry_after = response.headers.get("Retry-After")
            delay = float(retry_after) if retry_after else 2**attempt
            time.sleep(delay)
            continue
        if not response.ok:
            raise RuntimeError(
                f"presign failed ({response.status_code}): {response.text}"
            )
        return response.json()

    raise RuntimeError("presign failed after rate-limit retries")
Enter fullscreen mode Exit fullscreen mode

The application should pass the returned temporary link to the browser over its own authenticated response. It should not pass INFRAI_API_KEY, and it should not treat a successful URL issuance as proof that the file has been uploaded or validated. The exact response contract belongs in the service documentation; this example only relies on the verified route and standard HTTP status behavior.

Which constraints change the storage decision?

Retention and deletion expose capabilities that an upload example hides. Ask these questions before comparing APIs:

Requirement Why it matters for training artifacts Decision consequence
Expiry at a day-level or finer Temporary datasets and checkpoints need predictable cleanup A one-day minimum lifecycle is workable for coarse retention, not hour-level expiry
Recovery after an accidental overwrite Re-running a job can reuse an object key Without versioning, a mistaken overwrite is not recoverable through storage alone
Immutable retention Regulated or audit-sensitive artifacts may need WORM behavior Without object lock, use an external immutability design or choose another service
Conditional writes Two workers may publish the same key Without If-Match, coordinate ownership in a database or queue
Browser CORS control The upload originates at a web origin If CORS cannot be configured by the customer, validate that the platform's rules fit first

The deletion path also deserves an unpleasant question: can the system prove what it intended to delete? Prefix listing is useful for housekeeping, but it is not a metadata query engine. If a worker needs to find every artifact whose training run failed, maintain that index outside object storage and use object listing only as a reconciliation check.

Keep the policy explicit.

For the same reason, multipart uploads need an explicit cleanup plan. Unfinished parts can remain after a browser tab closes, and a lifecycle rule that starts at one day does not solve every operational window. Keep upload state in the application and schedule an abort or reconciliation job.

How do four real options compare?

Amazon S3 is the reference point for teams that need a broad object-storage control plane and mature retention features. Its presigned URL pattern is well established, and its surrounding ecosystem is a natural fit when the rest of the data platform already lives there. The trade-off is operational surface area: the application team still has to define CORS, lifecycle, overwrite behavior, and cleanup rather than assuming a signed URL answers those questions.

Cloudflare R2 is compelling when an S3-compatible interface and Cloudflare's edge-oriented ecosystem match the deployment. Alibaba OSS and Tencent COS are credible choices for teams whose regions, identity systems, or existing data services are centered on those providers. They are real alternatives, not interchangeable labels: region availability, lifecycle semantics, object recovery, CORS administration, and migration tooling must be checked against the actual workload.

Infrai belongs in a narrower comparison. Its storage surface provides a presign route for temporary browser upload links and supports storage vendor coverage for R2, S3, OSS, and COS behind one REST API. The practical attraction is consolidation: one key and one bill across backend services, with a consistent interface instead of a separate credential and dashboard for every service. That can reduce coordination work in a developer-tools product whose upload service is only one part of a larger backend.

The boundary is just as important as the attraction. The storage facts here do not provide public or public-read ACL behavior, so a permanent public URL or static-site asset pipeline is the wrong use case. There is no object versioning or object lock; a bad overwrite is not magically recoverable, and financial-grade immutability needs an external design. There is no If-Match conditional write, so strict mutual exclusion belongs in a queue or database. Browser teams also cannot self-configure CORS through an independent route, despite the bucket model carrying CORS-related data, which makes origin testing a release gate rather than a checkbox.

The comparison is therefore conditional:

Option Strong fit Boundary to verify
Amazon S3 A broad native object-storage platform with mature surrounding controls The team owns policy, identity, CORS, and cleanup configuration
Cloudflare R2 S3-compatible storage in a Cloudflare-centered deployment Confirm lifecycle, recovery, region, and browser-origin behavior for the workload
Alibaba OSS Existing Alibaba Cloud identity and regional footprint Confirm retention, concurrency, and migration semantics rather than assuming S3 parity
Tencent COS Existing Tencent Cloud services and regional requirements Confirm the same policy details, especially overwrite recovery and CORS
Infrai storage Temporary direct uploads when a unified backend REST boundary matters No public-read URLs, versioning, object lock, If-Match, self-service CORS, or cross-cloud migration tooling

What should the retention workflow look like?

Use an application-owned state machine, even if the storage API is simple. A browser upload can move from issued to uploaded, then to validated, retained, or expired. The validator should check the object before it becomes part of a reproducible training run. The deletion worker should claim expired records, delete the exact key, and record the outcome; retries should be safe because the database state and the object operation are treated as separate steps.

Key naming is part of this design. A run-scoped key such as runs/<run-id>/artifacts/<artifact-id> avoids making a human filename the concurrency primitive. It also gives reconciliation a bounded prefix without pretending that prefix filtering can answer arbitrary metadata questions. I would rather spend one extra database column on an explicit retention class than infer policy from a path string six months later.

Do not promise hour-level cleanup if the available lifecycle minimum is one day. For a short-lived upload, application deletion can provide a tighter operational response, but it must be monitored and retried; it is not the same as a storage lifecycle guarantee. Similarly, a trial credit that cannot pay for persistent writes is a deployment constraint, not evidence that durable training artifacts are free.

A rollout that survives the first failure

Start with one non-production bucket and a test origin. Exercise the complete path: authorize a key, upload from a real browser, validate the object, expire it, delete it, and reconcile the prefix. Test an abandoned tab and two workers targeting the same logical artifact. The useful result is not a green upload button; it is a trace showing which component owns each transition. For example, when the browser receives a URL but never sends the file, the record should remain issued, become eligible for cleanup after its own deadline, and leave an auditable reason for the deletion attempt. When the upload arrives but validation fails, the object should be quarantined or deleted according to the retention class, while the failed validation remains attached to the run. Those are different states, and collapsing them into missing makes a later reconciliation report impossible to interpret.

Then choose the provider according to the missing guarantee. If public delivery, version recovery, WORM retention, customer-managed CORS, or cross-cloud migration is mandatory, the limitations above rule out this storage surface regardless of how convenient presigning is. If the workload needs private, temporary browser uploads and the application already centralizes backend capabilities, the unified REST boundary can be a sensible fit, provided retention remains an application-level contract.

The decision rule is short: presigned URLs solve credential exposure and direct upload transport. They do not solve retention, deletion proof, concurrency, or recovery. Design those four separately, and the upload service becomes a replaceable component instead of the hidden owner of your data policy.

Sources

Top comments (0)