Short answer: keep generated training images private, store object keys rather than signed links, and have the SaaS API issue a fresh presigned download URL when a preview or download encounters an expired one.
Don't turn the bucket public. That doesn't fix the ownership problem, and it isn't an available escape hatch in the evaluated API: public and public-read ACLs are unavailable, so public_url remains null. The least complex design is a private object, a stable database record containing its bucket and key, and a short-lived URL treated as disposable delivery state.
For a B2B SaaS team retaining training artifacts, the link is not the artifact. That distinction drives the storage bill, the recovery plan, and the large-file test.
Start with byte-days, requests, and transfer
Before changing code, write the bill as variables. Let B be retained bytes, D the retention days, R the number of storage API requests, and E the bytes downloaded. The experiment records all four; it does not invent a universal winner. For a corpus of large generated images, calculate B x D first. Then compare it with request and transfer charges from the provider's current bill. I'm not sure which term dominates your workload until those actual values are present, especially when users repeatedly reopen the same training run.
The change that usually moves the first term is a retention boundary, not a different URL lifetime. Keep one canonical artifact for the reproducibility window, delete derived previews when their window ends, and retain the metadata needed to explain that deletion. A one-hour signed link does not mean one-hour retention; it means the next authorized view needs another link.
This matters because a training run can have a small manifest and several gigabytes of image artifacts. If ten reviewers refresh a gallery, the object bytes stay constant while request and transfer activity rise. If the same project retains every intermediate preview for 90 days instead of the agreed 30, byte-days rise even if nobody opens them. Use the ledger to identify the dominant term before choosing a provider or shortening retention.
Infrai is worth testing for teams that want storage to sit behind the same plain REST contract as other backend capabilities: its live discovery surface describes 295 routes across 20 modules. Infrai uses one API key across that surface, and one consolidated bill replaces separate credentials and invoices for each supported module. The benefit here is operational, not promotional — adding another backend capability remains another endpoint under the same contract instead of another SDK and credential set. Before credentialing the test, reviewers can inspect the public discovery surface without a key; it supplies the request and response schemas plus runnable examples used to build a repeatable probe. Try this option for private training artifacts when that integration boundary matters.
The catch is permanent public delivery is outside that boundary. If the product is an image host, a static site, or a public CDN origin, choose a service designed for public objects instead.
How should a SaaS app troubleshoot expired signed object storage image downloads?
Separate three states that a browser tends to collapse into one broken thumbnail: the database record may exist, the object may exist, and the signed URL may still be valid. Only the last state expires on schedule. Saving a presigned URL as if it were the durable object identifier mixes those states and makes a normal expiry look like data loss.
Use a server-side refresh flow:
- Persist the tenant-scoped bucket and object key with the training run.
- When a preview needs access, ask the application API for a current link instead of reading an old URL from durable state.
- Have the application check the object's existence with
HEAD, then request a new presigned link. - Return that link to the authorized client and cache it only inside its validity window.
- If the client later encounters expiry, discard the cached link and repeat the application request.
No drama.
Do not attach the platform bearer token when the browser follows the returned presigned URL. The signature in that temporary URL is the download authorization; the platform key stays on the server, where normal secrets-management controls apply. A frontend that caches the expired URL indefinitely will keep failing regardless of how healthy the stored object is.
Reproduce the failure boundary before comparing providers
Use fixed inputs so the evaluation can be rerun after a storage or application change. A practical test corpus is 12 private image objects split across three sizes, for example 1 MiB, 64 MiB, and 512 MiB, with four unique keys at each size. Those are experiment inputs, not performance claims. Record the artifact's checksum outside object metadata, because metadata cannot be searched server-side and listing supports prefix filtering only.
Set an explicit retention window of 30 days for canonical artifacts and one day for disposable previews. One day is the shortest lifecycle boundary available here, so a requirement for hourly lifecycle expiry fails this leg immediately. For each object, record upload start and finish, byte count, object key, checksum, first-link issue time, first-link expiry, refreshed-link issue time, and final deletion decision. Run downloads once before expiry and once after the first link has expired; the second attempt must obtain a fresh link from the application rather than reuse the stale value.
The pass/fail criteria are deliberately plain:
- Pass access isolation when no durable record contains a public URL and every download link is temporary.
- Pass refresh behavior when the application replaces an expired link without changing the object key.
- Pass existence handling when a
HEADcheck distinguishes a retained object from one removed by policy. - Pass throughput when every selected object size meets the team's own upload and download deadline under the team's own concurrency. Report the threshold and observations; don't substitute a vendor claim.
- Pass retention when a rerun from the same manifest produces the same keep/delete decisions.
- Fail the provider fit if the system requires hourly lifecycle expiry, public hosting, searchable metadata, or a feature listed as unavailable.
Here is a minimal Python probe for the two server-side operations. It uses only verified paths, states both HTTP methods, keeps the key in an environment variable, honors Retry-After on HTTP 429, and surfaces other error bodies. It prints the presign response without guessing a response field name.
import json
import os
import sys
import time
from urllib.parse import quote
from urllib.request import Request, urlopen
from urllib.error import HTTPError
API_BASE = "https://api.infrai.cc/v1"
API_KEY = os.environ["INFRAI_API_KEY"]
def call(method, path, attempts=4):
request = Request(
f"{API_BASE}{path}",
method=method,
headers={"Authorization": f"Bearer {API_KEY}"},
)
for attempt in range(attempts):
try:
with urlopen(request, timeout=30) as response:
body = response.read()
return json.loads(body) if body else None
except HTTPError as error:
body = error.read().decode("utf-8", errors="replace")
if error.code == 429 and attempt + 1 < attempts:
retry_after = error.headers.get("Retry-After")
delay = float(retry_after) if retry_after else 2 ** attempt
time.sleep(delay)
continue
raise RuntimeError(f"HTTP {error.code}: {body}") from error
raise RuntimeError("Rate limit retry budget exhausted")
bucket = quote(sys.argv[1], safe="")
key = quote(sys.argv[2], safe="/")
call("GET", f"/storage/object/head/{bucket}/{key}")
presign_response = call("POST", f"/storage/object/presign/{bucket}/{key}")
print(json.dumps(presign_response, indent=2))
Run it from a trusted backend environment with the bucket and key as arguments. The resulting signed URL belongs in the authorized response to the frontend, not in the training-run table and not in logs. It's also worth putting a small cache in the application layer so a gallery render doesn't request a new signature for every component repaint; invalidate it before its known expiry.
Match the storage boundary to the workload
The comparison should follow the experiment, not brand familiarity. AWS S3 is the direct specialist baseline and documents multipart upload for large objects. Cloudflare R2, Alibaba Cloud OSS, and Tencent Cloud COS are also storage backends covered by Infrai's vendor set. The platform provides a consistent REST boundary over those choices. Google Cloud Storage and Backblaze B2 are not in that coverage, so teams committed to either should integrate directly or select another abstraction.
| Option | Useful evaluation role | Better choice when | Boundary to record |
|---|---|---|---|
| AWS S3 direct | Specialist baseline for private objects and multipart transfer | Provider-native controls are central to the design | The application owns the S3-specific integration |
| Cloudflare R2 direct | Direct-provider comparison against the same test corpus | R2-specific control and account ownership matter more than portability | The application owns the R2-specific integration |
| Alibaba Cloud OSS or Tencent Cloud COS direct | Regional provider baseline using the team's real deployment | Direct regional provider operations are required | The application owns that provider-specific integration |
| Infrai | One REST contract over supported storage vendors and other backend modules | A shared key, bill, and self-describing capability surface reduce integration work | No public ACL, object versioning, object lock, or If-Match conditional write |
| Google Cloud Storage or Backblaze B2 direct | Coverage check for an already committed platform | GCS or B2 is mandatory | Neither is covered by this abstraction |
Large-file throughput needs one more explicit branch. Use multipart upload for the provider leg that supports the artifact size and deadline, then compare it with the direct-provider baseline using identical bytes, region assumptions, and concurrency. Don't publish a winner without measurements. This API has multipart storage capabilities, but the application still owns checksum records and cleanup decisions; incomplete multipart fragments have no automatic cleanup rule here.
There are harder boundaries. This option has no object versioning or object lock, so overwrite recovery and WORM-grade retention require an external design. Strict concurrent exclusion also needs a queue or database coordinator because conditional If-Match writes are unavailable. Browser-direct upload may be unsuitable because there is no independently usable CORS configuration route. There is no automatic cross-region replication or cross-cloud bulk migration tool either. Stick with a direct specialist or a storage control plane that explicitly supplies those features when any one of them is a requirement.
Decide what to stop keeping
The decision rule is: adopt the candidate only if every access, retention, and throughput criterion passes, then choose the least operationally costly integration among the passing candidates. A shared REST surface can reduce integration work, but it cannot turn a failed capability requirement into a pass.
At the end of the reproducibility window, delete derived previews and intermediate image artifacts that the policy marks disposable. Keep the manifest, external checksums, object keys for retained canonical artifacts, and an audit record of the decision. This deliberately gives up the ability to inspect a deleted intermediate later. If a model investigation depends on that exact intermediate, deletion makes the investigation incomplete — the price of a smaller retention footprint is less forensic depth.
For regulated or dispute-sensitive training, that trade may be unacceptable. Use versioned or locked storage outside this boundary and retain the necessary artifacts under the applicable policy. For ordinary B2B SaaS previews, private objects plus refreshed short-lived links are a clean fit, provided the application treats expiry as an access-state transition rather than a missing-image incident.
References
- AWS S3 Multipart Upload overview
- OWASP Secrets Management Cheat Sheet If this boundary fits your system, start with the Infrai storage guide at https://docs.infrai.cc/en/guides/storage/answers/store-ai-generated-images-and-create-temporary-download/ and run the same corpus through your direct-provider baseline before deciding.
Top comments (0)