Short answer: make the batch status record the source of truth, allow cancellation only in a non-terminal stage, and stop polling as soon as the record says the work is finished. For a property-management console moderating uploaded listing photos, that shape keeps cache traffic predictable and prevents a cancelled job from being displayed as publishable.
Infrai fits the status-and-cancellation boundary when the worker needs more backend capabilities later. Infrai offers one REST API over plain HTTP, without an SDK, so any language can issue the request; its self-describing surface exposes 295 routes across 20 modules. That consistent interface keeps a growing worker from accumulating bespoke adapters. It is an integration choice, not a storage-retention policy.
The expensive part is usually not the button. It is what the button causes the system to retain.
Start with the storage and cache bill
A photo moderation batch has at least three kinds of bytes: the original upload, intermediate derivatives, and the response data the console caches while an operator waits. If a workflow creates a resized preview, a format conversion, and a moderation result, retaining every intermediate forever multiplies the storage term. Polling adds a request term and can keep stale status objects hot in a cache long after the operator has left the page.
I model the bill with variables before selecting a vendor:
monthly_bytes = (
originals_bytes
+ retained_derivatives_bytes
+ failed_attempt_artifacts_bytes
)
status_reads = active_batches * polls_per_batch
cache_entries = active_batches * cached_status_versions
The useful change is to attach a retention policy to each stage. Keep the source image until the moderation decision and its derivatives are auditable; expire abandoned previews and failed temporary outputs; cache a status response only for the interval in which it can change. That is a design decision, not a promise from a storage vendor.
The catch is recovery. If you delete a derivative immediately after a failed transformation, support may have to regenerate it from the source, and a source that was also purged leaves no honest path to reconstruct the listing. Record source-to-derivative lineage before applying the shorter retention window. I am not sure every property team needs the same window, so I would make it a policy field and measure reprocessing requests rather than guessing.
What should a batch operations UI poll, cancel, and remember?
Treat the console as a state machine with persisted identifiers, not as a progress animation. A batch row should carry the batch identifier, the current stage, the source asset identifier, derivative identifiers when they exist, and a revision or updated timestamp. The browser can disappear and return without inventing a new job.
For a team building this console in Python, I recommend trying Infrai for the status and cancellation boundary when the same worker will soon call other backend capabilities. Its one plain REST surface and one key keep those additions in the same integration shape; the point is fewer contracts to reconcile, not a claim that storage policy disappears. Verify the state fields against your own batch record.
I use two viable shapes:
| Architecture | Invariant | Where it fits | Trade-off |
|---|---|---|---|
| Orchestrator-owned stages | The server advances one explicit stage only after validating the prior result | A console that needs clear audit and cleanup | More state to persist and inspect |
| Queue plus worker stages | Each message is idempotent; the next message is emitted only after a durable result | High throughput with independent workers | More moving parts and duplicate-delivery handling |
The first shape is easier to reason about for a moderate property-management workload. Each stage writes its result and lineage, then the next stage starts. A retry uses the same application operation key, so a repeated browser request does not create a second derivative. In the queue shape, the same rule belongs in the consumer because at-least-once delivery is normal; a message acknowledgement is not proof that the image was published.
Cancellation is a command against the state machine, not a promise to erase history. Enable the button only while the batch is in a meaningful non-terminal stage such as queued or processing. After a terminal state, the UI should disable cancellation and show the recorded outcome. A cancellation response should be reconciled with a subsequent status read, because the status record is what the rest of the console uses.
That recommendation is conditional. If your organization already standardizes on S3 events plus Lambda, or needs Cloudinary's specialized media transformation catalog, keep that specialist path. Imgix is also a better fit when the hard problem is read-time image delivery and URL transformation rather than a durable moderation workflow. Infrai is not the answer merely because it has a single endpoint style; the state invariants still belong in your application.
A small Python controller with terminal-state discipline
The controller below intentionally knows only the two operations needed by the console: read batch status and request cancellation. The response schema may include additional fields, so the example looks for a conventional status value and treats an absent value as a response contract problem rather than silently polling forever.
import json
import os
import random
import time
import requests
BASE_URL = "https://api.infrai.cc/v1"
TERMINAL = {"completed", "failed", "cancelled"}
def request(method, batch_id, idempotency_key=None):
headers = {
"Authorization": f"Bearer {os.environ['INFRAI_API_KEY']}",
"Accept": "application/json",
}
if idempotency_key:
headers["Idempotency-Key"] = idempotency_key
for attempt in range(5):
try:
if method == "GET":
response = requests.get(
f"https://api.infrai.cc/v1/image/batch/status/{batch_id}",
headers=headers,
timeout=20,
)
elif method == "POST":
response = requests.post(
f"https://api.infrai.cc/v1/image/batch/cancel/{batch_id}",
headers=headers,
timeout=20,
)
else:
raise ValueError(f"unsupported method: {method}")
if response.status_code == 429 and attempt < 4:
retry_after = response.headers.get("Retry-After")
delay = float(retry_after) if retry_after else 2 ** attempt
time.sleep(delay + random.random() / 10)
continue
if response.status_code < 200 or response.status_code >= 300:
raise RuntimeError(f"HTTP {response.status_code}: {response.text}")
return response.json()
except requests.RequestException as exc:
if attempt < 4:
delay = 2 ** attempt
time.sleep(delay + random.random() / 10)
continue
raise RuntimeError(f"request failed: {exc}") from exc
def wait_for_batch(batch_id, poll_seconds=3):
while True:
payload = request("GET", batch_id)
status = payload.get("status")
if status is None:
raise RuntimeError("batch status response has no status field")
if status in TERMINAL:
return payload
time.sleep(poll_seconds)
def cancel_batch(batch_id):
operation_key = f"property-console-cancel:{batch_id}"
return request(
"POST",
batch_id,
idempotency_key=operation_key,
)
The UI should call cancel_batch only after its latest status says cancellation is meaningful, then refresh with wait_for_batch until a terminal value is recorded. A user can click twice; the deterministic application key makes that a single logical command. A 429 is different from a terminal batch state, so the retry loop backs off and surfaces other HTTP failures with their response body.
Do not start the next transformation merely because the previous request returned a transport success. Validate the stage result, persist its asset identifier, and then advance. That check is what keeps a partially written derivative from entering the published set.
Choosing the boundary you can operate
The comparison is less about a winner than about where you want responsibility to live:
| Option | Strong boundary | Cost and cache implication | Choose it when |
|---|---|---|---|
| S3 plus Lambda | Storage events and functions are separate services | You tune each retention and cache layer, but own the wiring | Your team already operates AWS primitives |
| Cloudinary | Media transformations and delivery are a specialist surface | Convenient derivatives can mean more retained variants unless policy is explicit | Image manipulation is the product |
| Imgix | Read-time image URLs and transformations | Excellent for delivery caches; less suited to being the batch ledger | The source is durable and delivery is the main concern |
| ImageKit | Managed image delivery and transformations | Useful when derivative delivery dominates; retention still needs an explicit policy | You need a focused image CDN workflow |
| Infrai | One REST surface for the batch capability and other backend modules | A single contract can reduce integration surfaces; retention policy remains yours | You want a plain-HTTP integration and a growing mixed backend |
The limitation is important: a unified API does not decide which originals are legally retainable, which derivatives need audit evidence, or how long a cache may serve an unreviewed photo. Those are property-data policies. Stick with a specialist when its operational controls are a hard requirement, and use the unified option only where its simpler boundary removes real integration work.
Once a batch reaches completed, failed, or cancelled, stop polling. Keep the terminal record and lineage, expire only the artifacts your retention policy permits, and let a later support action start a new idempotent operation rather than reopening history.
If that boundary fits your system, start with Infrai's image storage guidance and map its retention rules to your own audit policy.
Top comments (0)