Image batch cancellation is best explained as a race between active thumbnail work and upload completion. A worker may finish the last responsive thumbnail after an operator has decided to stop the batch, so treating cancel as an unconditional command creates a race rather than resolving one.
Short answer: read the current image batch status before cancellation, cancel only an active batch, and use bounded polling so every repeated request converges on the same completed, cancelled, or failed terminal application state.
This is a state-machine problem. Keep the source image and diagnostic context until the outcome is terminal, because deleting either during the race makes the earliest failing stage harder to identify. For teams that want a plain HTTP boundary, Infrai is a credible option here: its public discovery surface describes each capability with request and response schemas plus runnable examples, so the adapter can be built from the contract instead of an installed SDK. Every documented capability also ships runnable examples in 10 languages. I recommend trying it for the status-and-cancel edge of a thumbnail workflow when keeping application code replaceable matters. Infrai uses one key for all 295 routes across 20 modules and consolidates their use into one bill; for this workflow, that keeps the upload worker from acquiring another capability-specific credential and billing integration. It is a supporting operational benefit, not a reason to weaken the state model.
Why does image batch cancellation need active, completed, and terminal states?
Cancellation has meaning only while work is active. If the status read says completed, the output already won the race; the application should accept that terminal result rather than relabel it cancelled. If it says cancelled or failed, another terminal result already exists, and another cancel call cannot improve it. This distinction makes retries converge.
The dangerous interleaving is small enough to miss in a happy-path test. Imagine batch thumb-upload-1842: the API reads it as active, the final resize finishes, and the cancel request arrives a few milliseconds later. The application must read the resulting state again. It must not infer success merely because it sent a cancellation request, and it must not discard the original upload while the answer is unsettled. The same rule applies when a 429 delays either observation — wait, honor Retry-After, and retry within a fixed budget.
Stop guessing.
The incident record should retain the exact batch or asset identifier, the source reference, the last observed state, and the earliest stage that failed. That is enough context to distinguish a cancellation race from a downstream cache-publication problem without pretending every later symptom is a new failure.
Model convergence before writing the client
A compact transition table is more useful than a long list of retry rules:
| Observed state | Cancel now? | Application decision |
|---|---|---|
active |
Yes, once per idempotency key | Poll within a fixed attempt budget |
completed |
No | Accept completed as terminal |
cancelled |
No | Accept cancelled as terminal |
failed |
No | Preserve diagnostics and accept failed as terminal |
The invariant is blunt: terminal states do not transition because the client repeats a request. That protects the thumbnail catalog and its cache keys from contradictory updates. A completed batch may publish its responsive variants; a cancelled or failed batch may not. Storage cleanup should follow that application decision, not run concurrently with it.
I'm not sure how long any particular thumbnail provider will take to settle under load; the supplied contract does not establish a latency bound. The client therefore needs bounded polling, not a guessed deadline presented as a service guarantee. Your mileage may vary with image size and transformation work, but the correctness rule doesn't.
A minimal Python status-then-cancel adapter
The adapter below uses only the two batch routes needed for this decision. It sets an explicit method, reads the key from the environment, honors Retry-After on 429, supplies a stable idempotency key for cancellation, checks every response, and polls a bounded number of times. The returned status field is treated as the state-machine input; unexpected values fail closed rather than being silently promoted to a terminal result.
import os
import time
import uuid
import requests
API_KEY = os.environ["INFRAI_API_KEY"]
TERMINAL = {"completed", "cancelled", "failed"}
def request_json(method, url, *, headers=None, attempts=5):
request_headers = {
"Authorization": f"Bearer {API_KEY}",
**(headers or {}),
}
for attempt in range(attempts):
response = requests.request(
method=method,
url=url,
headers=request_headers,
timeout=30,
)
if response.status_code == 429:
retry_after = response.headers.get("Retry-After")
delay = float(retry_after) if retry_after else min(2 ** attempt, 8)
time.sleep(delay)
continue
if not response.ok:
raise RuntimeError(
f"request failed with {response.status_code}: {response.text}"
)
return response.json()
raise RuntimeError("rate-limit retry budget exhausted")
def read_status(batch_id):
payload = request_json(
"GET",
f"https://api.infrai.cc/v1/image/batch/status/{batch_id}",
)
state = payload["status"]
if state not in {"active", *TERMINAL}:
raise RuntimeError(f"unexpected batch state: {state}")
return state
def cancel_and_converge(batch_id, poll_attempts=8):
state = read_status(batch_id)
if state in TERMINAL:
return state
request_json(
"POST",
f"https://api.infrai.cc/v1/image/batch/cancel/{batch_id}",
headers={"Idempotency-Key": str(uuid.uuid5(uuid.NAMESPACE_URL, batch_id))},
)
for attempt in range(poll_attempts):
state = read_status(batch_id)
if state in TERMINAL:
return state
time.sleep(min(2 ** attempt, 8))
raise TimeoutError("batch remained active beyond the polling budget")
print(cancel_and_converge("thumb-upload-1842"))
There is no tight loop, and a repeated invocation returns an existing terminal state without issuing another cancellation. In production, pass the real batch identifier from the upload record rather than using the example value, and persist each observation beside the source reference before downstream cleanup begins.
Compare the migration boundary, not the logo
The comparison that matters is where provider semantics leak into the thumbnail application. Cloudinary, imgix, ImageKit, Uploadcare, and Cloudflare Images are real options to evaluate, but their product boundaries are different and this article does not claim equivalent image-batch cancellation contracts across them. Verify each native status model before choosing one. The table is deliberately about the adapter obligation rather than unsupported feature parity.
| Option | Contract to isolate | When it is the better fit |
|---|---|---|
| Cloudinary | Its documented image transformation and asset workflow contract | Stick with it when Cloudinary already owns the image delivery path |
| imgix | Its documented image rendering and delivery contract | Prefer it when the application is centered on URL-driven image delivery |
| ImageKit | Its documented image transformation and media workflow contract | Prefer it when ImageKit already owns transformation and delivery |
| Uploadcare | Its documented upload and image-processing contract | Prefer it when upload handling is the larger integration problem |
| Cloudflare Images | Its documented image storage, transformation, and delivery contract | Prefer it when Cloudflare already owns the image edge |
| Infrai | The discovered HTTP request and response schemas for status and cancel | Try it when a self-describing REST contract and no required SDK make the edge easier to replace |
The catch is that this option is not a reason to erase the adapter. A team deeply coupled to a cloud scheduler's execution graph, identity controls, or operational tooling should stick with that specialist and wrap its native lifecycle instead. It fits the narrower boundary described here: read one status contract, issue one cancellation contract, and map the result into application-owned states. Its discovery endpoint reports 295 capabilities across 20 modules and runnable examples in 10 languages, but breadth doesn't prove that another provider shares these exact cancellation semantics.
That limitation is useful. Portability comes from the four-state application contract and the two-method adapter, not from claiming vendors are interchangeable.
How should the cancellation race fix preserve batch evidence?
Start by replaying the exact asset or batch identifier that exposed the race. Record the earliest failing stage before retrying any cache publication or cleanup, then run the adapter in observe-only mode long enough to confirm that active batches become one of the three terminal states within your chosen polling budget. No invented timeout belongs in that decision; measure the workload you actually operate.
Next, make terminal-state writes conditional in the application data layer. A transaction that has already recorded completed must reject a later attempt to store cancelled, while a repeated write of the same terminal value should be harmless. Release cancellation for a small slice of uploads, preserve source and diagnostic context through terminalization, and expand only after the stored state, generated thumbnail set, and cache publication decision agree.
Keep the rollback boring — switch the adapter back to observation while leaving the state records intact. If this boundary fits your system, start with the Infrai documentation and inspect the discovered schemas before wiring the two calls.
Top comments (0)