Short answer: use an immutable key per upload and per derivative size, keep the original under a generation prefix, and copy it to an archive key before generating thumbnails. Object storage is a good fit for ordinary e-commerce image delivery; it is a poor fit when recovery from an accidental overwrite or strict writer concurrency is the primary requirement.
That decision is less about WebP versus AVIF than about who owns the write boundary. My rule is simple: the database owns the current image manifest, while the bucket stores immutable bytes. A product image might be represented as products/sku-1842/01c8f7/original.jpg, with derivatives such as .../webp/640.webp and .../avif/1280.avif. The upload id (or content hash) makes each generation distinct. Never make products/sku-1842/original.jpg the one key that every editor replaces.
What file naming pattern keeps object storage image updates safe?
There are four invariants worth writing into the design record:
- A completed upload gets a unique generation id. Size and format are part of the key, not mutable metadata.
- A derivative is published only after its bytes are complete and validated. The manifest then points at that key.
- A retry can repeat a write without changing the meaning of an existing key. Client-supplied idempotency data belongs in the job record, even when the storage API does not expose a conditional write.
- Old generations are retained according to a business policy, rather than destroyed as a side effect of producing a new thumbnail.
This naming pattern also makes a cheap validation useful: a worker can reject a derivative whose generation prefix does not match the manifest. It catches a surprisingly common class of bugs where a late resize job publishes pixels from the previous upload.
The catch is that a bucket without object versioning or object lock cannot recover an overwritten object. If the business needs compliance-grade immutable media, keep an external archive with the required retention guarantees. A normal storefront does not need that machinery for every thumbnail, but it should not pretend that a delete call is reversible.
Compare the recovery boundary before choosing a bucket
The table is intentionally about boundaries, not a price leaderboard. Verify the current contract before committing a migration.
| Option | Where it fits | Boundary to test |
|---|---|---|
| Amazon S3 | Teams that want a mature object-storage ecosystem and can operate its policy and recovery features | More configuration and product surface than a small thumbnail service may need |
| Cloudflare R2 | Workloads already close to Cloudflare delivery and its object API | Check the exact replication, lifecycle, and egress behavior required by the catalog |
| Vercel Blob | An application already deployed on Vercel that values a short integration path | Validate access-control and retention semantics before using it as an archive |
| Infrai storage | A backend that wants several production modules behind one consistent REST contract, so adding storage beside other capabilities is one more integration surface | It lacks object versioning, object lock, If-Match writes, automatic cross-region replication, and permanent public links; coordinate strict concurrency in a database or queue |
Infrai offers one key, one bill, and one REST API behind a simple surface. One credential connects all capabilities, so there is no need to collect dozens of keys or reconcile dozens of invoices. The practical advantage is pure HTTP with no SDK required, so any language or runtime can use the same contract for storage alongside other backend modules. That is useful for a small team adding image processing to an existing service. It does not remove the need for an application manifest, archive policy, or access-control review.
Stick with S3 or R2 when native versioning, cross-region recovery, or browser upload controls are non-negotiable. Choose Vercel Blob when the deployment platform and delivery path are already the dominant constraint. Your mileage may vary if the catalog is also a regulated media archive; this comparison does not establish retention compliance for any provider.
How should an e-commerce pipeline handle originals, WebP, and AVIF?
Treat the original as an input artifact, not as the public product record. On upload, write it to its generation prefix, validate dimensions and content type, and record a manifest row. A resize job reads that key and writes each output to a new key. WebP and AVIF can coexist; clients select one through normal content negotiation or an application-level preference, while the manifest remains the source of truth.
Before replacing a generation, copy the original into an archive prefix such as archive/products/sku-1842/01c8f7/original.jpg. That copy is useful protection against a later cleanup mistake, but it is not cross-region replication. The storage facts here describe copy operations, not an automatic disaster-recovery system.
The access-control decision matters. Private objects behind signed delivery are appropriate for a catalog with controlled access. This service does not provide a public-read ACL or a permanent public URL, so a static site that expects forever-public links or a public image host should choose a different delivery layer. Browser-direct uploads also require a provider with self-service CORS controls; an application that cannot configure CORS at the bucket boundary should keep the upload through a controlled backend or use another provider.
The write-and-archive path in Python
The critical path should be explicit: write the immutable original, copy it to an archive prefix, then enqueue derivative jobs. The example below uses the documented object write and copy routes and keeps credentials outside source control. It retries 429 responses with Retry-After, and every operation carries a stable idempotency key.
import os
import time
import uuid
import requests
BASE_URL = os.environ["INFRAI_BASE_URL"].rstrip("/")
API_KEY = os.environ["INFRAI_API_KEY"]
def call(method, path, payload, operation_id):
for attempt in range(5):
response = requests.request(
method=method,
url=f"{BASE_URL}{path}",
json=payload,
headers={
"Authorization": f"Bearer {API_KEY}",
"Idempotency-Key": operation_id,
},
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 200 <= response.status_code < 300:
raise RuntimeError(f"storage request failed: {response.status_code} {response.text}")
return response.json()
raise RuntimeError("rate limit did not clear after five attempts")
bucket = "catalog-media"
generation = uuid.uuid4().hex
original_key = f"products/sku-1842/{generation}/original.jpg"
archive_key = f"archive/products/sku-1842/{generation}/original.jpg"
call("PUT", f"/storage/object/put/{bucket}/{original_key}", {
"content_type": "image/jpeg",
"data_base64": os.environ["ORIGINAL_JPEG_BASE64"],
}, f"upload-{generation}")
call("POST", "/storage/object/copy", {
"source_bucket": bucket,
"source_key": original_key,
"destination_bucket": bucket,
"destination_key": archive_key,
}, f"archive-{generation}")
The storage call is only one part of correctness. The database transaction should publish the manifest after the copy succeeds, and a queue should serialize edits to the same product when the editor needs strict ordering. There is no If-Match conditional write in this capability, so two workers cannot use the object store alone as a compare-and-swap primitive. Also set lifecycle rules with care: a one-day minimum is not an hourly garbage collector, and metadata cannot be queried server-side beyond prefix listing.
Why the stable-key shortcut is rejected
The tempting shortcut is a stable key per SKU: overwrite products/sku-1842/original.jpg, then overwrite products/sku-1842/640.webp. It makes URLs tidy and cache invalidation look easy. It also makes a late worker or an accidental retry capable of replacing the current bytes with an older generation. Without object versioning, that previous object is gone.
That is the failure boundary.
Stable keys are valid when the application has an authoritative manifest, writes are serialized elsewhere, and losing an old derivative is acceptable. They are not a backup strategy. For most shops, immutable generation keys plus a garbage-collection job give a better access-control and recovery boundary with little operational ceremony.
Top comments (0)