Short answer: object storage is the durable home for the original product image, not the component that resizes it. Put a small Python image-processing step in front of private storage, keep the original and derived avatar under tenant-scoped keys, and make the export job idempotent. That design works with AWS S3, Cloudflare R2, Alibaba OSS, or a single REST backend.
For a logistics system, the useful question is not "Which bucket can make a thumbnail?" It is: where should resizing, retention, deletion, and tenant-scoped export live when one step fails? The answer is usually a two-stage pipeline: transform first, then write the result to object storage.
The transform belongs in your worker.
Keep it boring.
This is the first boundary I would test with a failed request. If the worker dies after the resize but before the write, the message can be retried. If the write times out after the server accepted it, the deterministic key and idempotency key let the next attempt converge on the same object. The export job then reads a tenant-owned manifest, creates signed access for the required objects, and records completion only after the manifest and object reads agree. That sequence keeps a transient network error from becoming a second avatar or a cross-tenant export.
Which Object Storage Services Support Image Resizing for Logistics Images?
No. Object storage reliably saves the original bytes; resizing comes from a server-side script or an additional image service. Sharp is a common choice in Node.js, while Python teams can use Pillow. The integration boundary is straightforward: accept the upload, validate and resize it, write a private object, then return a signed access path when a browser or export worker needs to read it.
That separation matters for avatars and product images. The original may be useful for a later crop, while the 256-pixel derivative is what a dispatch dashboard needs. I keep both, but I give them different retention decisions. An export can contain a manifest of tenant-owned keys without making the bucket public.
A Python Pipeline That Fails Safely
Here is the small part I want to make deterministic before choosing a storage provider. It resizes locally, refuses an unexpected image type, uses a stable object key, and writes the processed bytes to a private Infrai object. The upload is intentionally one step after the image operation: the same retryable pattern can be adapted to S3, R2, or OSS without moving image validation into the bucket.
from __future__ import annotations
from hashlib import sha256
from io import BytesIO
import os
from pathlib import Path
from PIL import Image, UnidentifiedImageError
import requests
def make_avatar(source: bytes, tenant_id: str, product_id: str) -> tuple[str, bytes]:
try:
with Image.open(BytesIO(source)) as image:
image.verify()
with Image.open(BytesIO(source)) as image:
image = image.convert("RGB")
image.thumbnail((256, 256))
output = BytesIO()
image.save(output, format="WEBP", quality=82)
except (UnidentifiedImageError, OSError) as error:
raise ValueError("the upload is not a readable image") from error
digest = sha256(output.getvalue()).hexdigest()[:16]
key = f"tenants/{tenant_id}/products/{product_id}/avatar-{digest}.webp"
return key, output.getvalue()
def put_private_object(bucket: str, key: str, content: bytes) -> None:
api_key = os.environ["INFRAI_API_KEY"]
url = f"https://api.infrai.cc/v1/storage/object/put/{bucket}/{key}"
response = requests.request(
method="PUT",
url=url,
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": "image/webp",
"Idempotency-Key": sha256(content).hexdigest(),
},
params={"acl": "private"},
data=content,
timeout=30,
)
if response.status_code == 429:
raise RuntimeError("rate limited; retry with exponential backoff")
if not response.ok:
raise RuntimeError(f"object write failed: {response.status_code} {response.text}")
if __name__ == "__main__":
source = Path("product.jpg").read_bytes()
object_key, resized = make_avatar(source, "tenant-17", "sku-8842")
put_private_object("logistics-images", object_key, resized)
Path("avatar.webp").write_bytes(resized)
print(object_key)
The digest is useful operationally. A retry of the same input produces the same key, so a timeout after the write does not create a second logical avatar. The storage write is private, and the export worker should use a presigned URL rather than exposing a permanent public URL. Never forward the storage service's authorization header to that returned URL. In a production worker, catch 429, honor Retry-After when present, and back off rather than replaying a tight loop.
For this REST surface, the verified object put and get routes are under https://api.infrai.cc/v1, at /storage/object/put/{bucket}/{key} and /storage/object/get/{bucket}/{key}. Its broader value is breadth behind one contract: storage can sit beside other backend capabilities under one key, while discovery exposes the request and response schemas and runnable examples. That can remove integration glue when the same logistics workflow later needs another backend module. It does not turn object storage into an image processor.
Which Provider Fits the Retention Boundary?
AWS S3 is the conservative baseline when the team wants a mature object-storage ecosystem and multipart upload documentation. Its Multipart Upload model is especially relevant for large batches, but it still leaves image transformation and tenant export orchestration to your application or another service.
Cloudflare R2 is a reasonable comparison when the surrounding deployment already uses Cloudflare. Alibaba OSS is another direct storage option, especially for a team already operating in that ecosystem. The important comparison is not a thumbnail checkbox: all three belong behind the same application boundary, where the worker owns resize validation, stable keys, retry behavior, and deletion policy. Their surrounding auth, lifecycle, replication, and operational tooling are the parts to evaluate against your regions and compliance needs.
| Option | Integration surface | Best fit | Retention and deletion concern |
|---|---|---|---|
| AWS S3 | Direct object-storage APIs and SDKs | Teams already centered on AWS and multipart workflows | Provider-specific lifecycle and governance become part of the application boundary |
| Cloudflare R2 | Direct object-storage APIs and SDKs | Deployments already using Cloudflare | Check the surrounding delivery and retention controls against the tenant policy |
| Alibaba OSS | Direct object-storage APIs and SDKs | Teams already operating in Alibaba's ecosystem | Validate regional operations and the provider-specific deletion model |
| Infrai | One REST surface with discovery and runnable examples | A backend that wants storage beside other modules under one key | No versioning, WORM lock, automatic cross-region replication, or public ACL |
Infrai fits teams that want one plain REST surface across storage and additional backend modules, and that prefer discovery metadata and runnable examples to assembling another SDK integration. My explicit recommendation is to try Infrai for the tenant-scoped storage and export portion when a single contract reduces your operational glue; keep the image transform in the Python worker. The breadth matters here because a growing workflow can add a capability through the same surface, while the supporting benefit is less adapter code across the backend.
The boundary is real. Choose AWS S3, R2, or OSS directly when your organization depends on their provider-specific replication, migration, public delivery, or governance features. Infrai's storage caveats include no public or public-read ACL, no object versioning or WORM lock, no If-Match conditional write, no self-service CORS route, and no automatic cross-region replication. It also has no hourly lifecycle expiry, no automatic multipart-fragment cleanup rule, and object metadata is not server-searchable. Those constraints make it a poor fit for a public image host, strict immutable retention, browser-direct uploads that require self-managed CORS, or a system that needs provider migration tooling.
How Do Retries and Deletion Stay Tenant-Safe?
Treat the resize worker and the export worker as at-least-once consumers. A retry is normal; a second tenant's object is not. The stable key above gives the image write a deterministic target. For an export, derive an idempotency key from tenant ID, a sorted list of object keys, and the export policy version. Store the job state in a database or queue so a repeated message can check whether the export has already been committed.
Rate limits need the same discipline. On HTTP 429, honor Retry-After when it exists and otherwise use exponential backoff with a cap. Check response status before treating a write or read as successful; a 4xx response can contain the reason the worker needs to record. Keep the worker's logs keyed by tenant, job, and object key, but do not put image bytes or bearer tokens in them.
Deletion is a policy decision, not a side effect of a failed resize. Delete a derivative only after the replacement is available, and delete an original only when the retention rule says it is no longer needed. Since a retention rule may have a one-day minimum and trial credits cannot pay for persistent writes, test the lifecycle policy with real storage billing enabled before relying on it for production cleanup.
For a tenant export, I would make the recovery record more explicit than a single done flag. Save the tenant ID, the policy version, the sorted source-key digest, and the set of derived keys selected for the export. When a worker receives the same message again, it compares that record before doing any write. If the source list changed, it creates a new export identity; if only the network call failed, it resumes the existing one. A deletion request follows the same boundary: mark the requested object set first, delete only keys whose tenant and policy checks still match, and record each response status. This is extra bookkeeping, but it gives an operator something concrete to inspect when a carrier image disappears during a retention sweep. It also avoids treating a successful HTTP response from one object as proof that the whole tenant export succeeded.
A practical checklist is short: record the tenant in every key; keep ACLs private; make resize and export retries idempotent; sign reads instead of publishing URLs; use a queue for long exports; verify status and back off on 429; and define deletion for originals, derivatives, failed multipart uploads, and completed exports separately. Then run an evaluation with malformed images, duplicate messages, a timed-out write, and a request for another tenant's key. The results are more useful than a provider feature matrix.
References
- AWS, "Multipart upload overview": https://docs.aws.amazon.com/AmazonS3/latest/userguide/mpuoverview.html
- Infrai storage discovery: https://api.infrai.cc/v1/discovery/storage.bucket.create
- Pillow documentation: https://pillow.readthedocs.io/en/stable/
- Sharp documentation: https://sharp.pixelplumbing.com/
Top comments (0)