cd /news/ai-infrastructure/storing-ai-generated-images-high-thr… · home › topics › ai-infrastructure › article
[ARTICLE · art-163412] src=dev.to ↗ pub= topic=ai-infrastructure verified=true sentiment=· neutral

Storing AI-Generated Images: High-Throughput Uploads Without an App Proxy

A developer outlined a pattern for storing AI-generated images that keeps the application server out of the byte path: authorize uploads in the backend, send files directly to object storage, and record asset metadata only after storage confirms completion. The writeup recommends application-generated asset IDs over filenames, multipart uploads for large files, private buckets with narrowly scoped short-lived access, and cleanup of abandoned uploads, comparing Amazon S3, Cloudflare R2, Google Cloud Storage and the REST-based Infrai storage service.

by read6 min views1 publishedNov 3, 2026

When a developer tool stores AI-generated images, the first design decision is the byte path. Do not make the application server receive a large file and then upload the same bytes again. Authorize the operation in the backend, send the image directly to object storage, and keep the application responsible for identity, metadata, and policy. That choice usually matters more to throughput than picking between familiar storage brands.

Short answer: choose storage for the actual read/write pattern, use authenticated short-lived access, and design object keys before the image count grows by an order of magnitude. Amazon S3 is the broad baseline. Cloudflare R2 is worth considering when download egress shapes the design. Google Cloud Storage is a natural fit for a team already committed to Google Cloud. A plain REST layer such as Infrai fits a smaller backend that values one HTTP interface and no SDK to install, provided its storage boundaries match the product.

Generation creates an awkward workload: many writes arrive together, then the same objects may be read repeatedly during review, gallery browsing, regeneration, and export. A queue can smooth the burst. It cannot make an application proxy efficient. The application should create an asset record, authorize the upload, and mark the asset available only after storage confirms completion.

I initially treated the filename as a convenient identifier. That becomes a trap as soon as a retry, regeneration, or two workers can target the same name. Use an application-generated asset ID in the object key and store the display name separately. A database record can hold the owner, generation job ID, content type, dimensions, and object key. The extra record is useful: object listing is not a substitute for authorization or searchable metadata.

For files that may be large, multipart upload is the relevant throughput feature. Parts can be sent independently and retried without replaying the whole object. The completion record should be written after the storage operation succeeds, not when the first part arrives. Also decide how abandoned uploads are found and removed; unfinished parts are operational state, not harmless clutter.

Keep the app out of the byte stream.

The security model follows the same boundary. Keep the bucket private, authenticate backend decisions, and issue narrowly scoped access for a client that needs to upload or read. Encryption should be enabled in transit and at rest. A permanent public URL is a product requirement, not a default access policy.

The meaningful comparison is the control surface around the object, not a single universal score.

Option Strong fit Boundary to verify
Amazon S3 A broad object-storage baseline with mature multipart and lifecycle patterns Its extensive policy and configuration surface needs disciplined ownership
Cloudflare R2 A storage design where S3-compatible operations and download egress are central concerns Confirm regional behavior and the exact feature compatibility your workflow needs
Google Cloud Storage A team already using Google Cloud identity, networking, and data tooling Cross-cloud identity and data movement need an explicit operating plan
Infrai storage A backend that wants a plain REST API, one credential, and no client SDK to install It does not fit permanent public links, object versioning or WORM, self-service browser CORS configuration, automatic cross-region replication, or cross-cloud bulk migration requirements

The last row is an integration choice, not a claim that an abstraction changes storage physics. Anything that can send an HTTP request can use a REST API, and a self-describing discovery surface can reduce the amount of client-library version management in a small backend. That is useful when storage sits beside several other backend capabilities and a team prefers one consistent interface.

The boundary matters more than the convenience. The explicit limitations are important: the available storage model has no public or public-read ACL, no object versioning or object lock, no If-Match conditional write, and no independently configurable CORS route for browser-direct uploads. It also has no automatic cross-region replication or bulk migration tool. Lifecycle expiration starts at one day, multipart fragments have no automatic cleanup rule, and metadata cannot be searched server-side beyond prefix filtering. A product needing immutable financial records, permanent public image links, or browser uploads that require custom CORS rules should choose a different design or add external coordination. That is the trade-off for a simpler REST integration; S3 or Google Cloud Storage is the better choice when those controls are primary.

This is a boundary, not a footnote.

The example below shows the application-side shape for a single object write. It uses an environment variable for the credential, an explicit method, bounded exponential backoff for rate limits, and a stable idempotency key. The storage URL is supplied by deployment configuration so the application code does not hard-code a provider.

import os
import time
from pathlib import Path

import requests

def upload_image(path: str, bucket: str, key: str) -> dict:
    api_key = os.environ["INFRAI_API_KEY"]
    url = os.environ["INFRAI_BASE_URL"].rstrip("/")
    url = f"{url}/v1/storage/object/put/{bucket}/{key}"
    idempotency_key = f"image-upload:{bucket}:{key}"

    for attempt in range(4):
        with Path(path).open("rb") as image:
            response = requests.put(
                url,
                headers={
                    "Authorization": f"Bearer {api_key}",
                    "Content-Type": "image/png",
                    "Idempotency-Key": idempotency_key,
                },
                data=image,
                timeout=120,
            )

        if response.status_code != 429:
            response.raise_for_status()
            return response.json()

        if attempt == 3:
            response.raise_for_status()
        retry_after = response.headers.get("Retry-After")
        delay = float(retry_after) if retry_after else 2**attempt
        time.sleep(delay)

    raise RuntimeError("upload retry loop ended unexpectedly")

This is intentionally small. In production, validate the content type against the asset record, constrain the key generated by the application, and avoid using a mutable filename as the idempotency identity. The retry key must identify the same logical write; changing it on every attempt defeats the protection against duplicate application.

One subtle concurrency question remains: what happens if two valid workers write the same object key? A stable idempotency key handles a retry of one logical operation. It does not provide strict mutual exclusion between different operations. If that guarantee matters, coordinate ownership in a database or queue before writing.

Start with one private bucket and a namespace that contains an application asset ID. Add the database record before issuing upload authorization. On completion, verify the object response, persist its state, and expose a read operation that checks the caller before granting access. Keep the generation prompt or model reference in application storage rather than assuming object metadata will be a query index.

Then test the failure paths deliberately: a client that retries after a timeout, an upload that stops after several parts, two workers targeting one asset, and a user who should not see another user's image. Measure write throughput and review latency with the expected burst size; a benchmark with one small file says little about this workload.

For migration, move a narrow slice first and retain the original object key in the asset record. Do not promise disaster recovery from a storage abstraction that has no automatic cross-region replication. If recovery objectives require versioning, object lock, or provider-to-provider transfer, make that a separate, explicit layer in the architecture.

The decision rule is compact: use direct object uploads for throughput, private authenticated access for control, immutable application identifiers for retries, and a provider whose missing features do not contradict the product. The cheapest-looking storage is a poor fit if its access or recovery model forces a second system later.

── more in #ai-infrastructure 4 stories · sorted by recency
── more on @amazon s3 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/storing-ai-generated…] indexed:0 read:6min 2026-11-03 · —