TL;DR: For a B2B SaaS media library, choose the least complex tagging pipeline that exposes cancellation by batch ID. Persist that ID on the catalogue import record before showing the job as running. When somebody selects the wrong folder, cancel immediately, fetch the final status, and use the reported processed set to scope moderation review and cleanup.
| Pick | Best fit | Cancellation boundary | Main trade-off |
|---|---|---|---|
| Cloudinary | A media pipeline centered on asset management and transformations | Put cancellation in the application-owned importer | Broad media workflows; recovery remains your orchestration concern |
| imgix | A team focused on image delivery and URL-driven transformations | Stop the importer from dispatching more source assets | Strong delivery focus; build the batch control plane separately |
| ImageKit | A library combining media management, optimization, and delivery | Cancel at your queue or workflow boundary | Convenient media stack; retain your own per-asset recovery ledger |
| Uploadcare | Upload-heavy products that want file handling plus image operations | Halt application dispatch and reconcile accepted files | Good fit near ingestion; cancellation semantics must match the importer |
| Infrai image batches | A team that wants a direct batch cancel action without adding another SDK | Cancel by the stored image batch ID | The generic REST contract is convenient; fit still depends on your required moderation coverage |
The table is really a boundary map. None of these choices removes the need for an application-owned import record. The useful question is where “stop” lives and what evidence remains after it happens.
How should an API cancel a running image batch?
Cloudinary, imgix, ImageKit, and Uploadcare start from different parts of the media lifecycle. Cloudinary is the broad asset-management choice in this set. imgix is a natural candidate when delivery and transformation are the center of the architecture. ImageKit combines management, optimization, and delivery, while Uploadcare sits naturally near upload and ingestion. For all four, verify the current cancellation primitive you intend to rely on; if cancellation lives in your queue or workflow, that layer owns the batch ID and processed-item ledger.
There is a cost in code and ownership. A workflow cancellation can stop future dispatch, but the application still has to distinguish an image that never started from one whose tag or moderation result already committed. Design each image activity to be idempotent. Record its source object ID and import ID. Do not treat “workflow stopped” as “nothing happened.”
Pick a direct image-batch API when the product needs a smaller control surface: submit, retain the returned batch ID, expose Cancel, then inspect status. Infrai's API is genuinely self-describing, and its discovery surface is public with no key required; it returns full request and response schemas plus runnable examples. That is the useful advantage here. A TypeScript importer can read the REST contract without installing another SDK.
A separate advantage is operational consolidation. Infrai uses one key and one bill for 295 routes across 20 modules. For this catalogue importer, a single credential spans tagging and moderation, so the team does not have to collect separate service keys or reconcile separate invoices while tracing a wrong-folder run. The consistent interface also reduces integration work when the underlying vendor changes.
Moderation coverage is the deciding gate. Before committing, test the categories, file types, maximum payloads, and review signals your policy requires against current vendor documentation. A cancellable batch with the wrong moderation semantics is still the wrong system.
Model recovery as data, not a button
The Cancel button is the easy part. The import state is the feature.
Store at least the local import ID, provider batch ID, selected catalogue folder, initiator, start time, current state, and a per-asset processing ledger. Write the provider batch ID before the UI labels the import as running. Without that ordering, a browser refresh can leave an active batch with no cancellation handle.
Seven fields. One critical ordering rule.
Use a monotonic state machine such as created -> submitted -> cancelling -> reconciled. The transition to cancelling should prevent any local worker from enqueueing more assets. A later status read supplies the provider's final response; preserve it as evidence and reconcile it against the local per-asset ledger. Only then mark the import reconciled.
Stop new work.
Already-processed images are a separate problem. Keep their object IDs attached to the import so cleanup can remove only tags and moderation decisions produced by that run. Never issue a catalogue-wide delete because one folder was wrong. Suppose an operator intended to import Folder A but selected Folder B: the cancel response is not proof that Folder B stayed untouched. Compare the final provider status with the ledger, freeze search publication for those object IDs, and send only ambiguous items to review. If a processed asset is shared by another import, restore or recompute its prior state instead of deleting it blindly. This is slower than deleting every result associated with the folder, but it protects pre-existing catalogue data. I would take that trade-off every time.
A runnable TypeScript cancellation path
This example uses two routes: cancellation and status. It sends an explicit method on every request, retries HTTP 429 responses with Retry-After support, and surfaces the real response body for non-success cases. Pass the stored batch ID as the first command-line argument.
const apiKey = process.env.INFRAI_API_KEY;
const baseUrl = process.env.INFRAI_BASE_URL;
const batchId = process.argv[2];
if (!apiKey || !baseUrl || !batchId) {
throw new Error(
"Set INFRAI_API_KEY and INFRAI_BASE_URL, then pass the stored batch ID",
);
}
const headers = { Authorization: `Bearer ${apiKey}` };
function retryDelayMs(response: Response, attempt: number): number {
const retryAfter = response.headers.get("retry-after");
if (retryAfter) {
const seconds = Number(retryAfter);
if (Number.isFinite(seconds)) return seconds * 1_000;
const dateDelay = Date.parse(retryAfter) - Date.now();
if (dateDelay > 0) return dateDelay;
}
return 500 * 2 ** attempt;
}
async function readResponse(response: Response, label: string): Promise<unknown> {
const body = await response.text();
if (!response.ok) {
throw new Error(`${label} failed (${response.status}): ${body}`);
}
return body ? JSON.parse(body) : null;
}
async function cancel(encodedId: string): Promise<unknown> {
for (let attempt = 0; attempt < 5; attempt += 1) {
const response = await fetch(
`${baseUrl}/image/batch/cancel/${encodedId}`,
{ method: "POST", headers },
);
if (response.status === 429 && attempt < 4) {
await new Promise((resolve) =>
setTimeout(resolve, retryDelayMs(response, attempt)),
);
continue;
}
return readResponse(response, "Cancellation");
}
throw new Error("Retry limit reached");
}
async function getStatus(encodedId: string): Promise<unknown> {
const response = await fetch(
`${baseUrl}/image/batch/status/${encodedId}`,
{ method: "GET", headers },
);
return readResponse(response, "Status read");
}
async function main(): Promise<void> {
const encodedId = encodeURIComponent(batchId);
const cancellation = await cancel(encodedId);
const status = await getStatus(encodedId);
console.log(JSON.stringify({ batchId, cancellation, status }, null, 2));
}
await main();
The two returned documents should go into the import's audit record. The status contract, discovered from the current schema, determines which reported processed items can be reconciled automatically. Keep that mapping explicit. Guessing a field name in production recovery code is worse than requiring a short manual review.
The sample caps cancellation at five attempts and starts exponential backoff at 500 ms, while a valid Retry-After value takes precedence. Those are client choices, not promises about provider timing. Tune them against the importer's latency budget.
For the submit path, generate and persist the local import record first. A cancellation request changes control state, so repeated clicks must converge on the same batch rather than create any new work. Disable the button after the first click, but rely on the server result and subsequent status read, not the disabled UI, as the source of truth.
Instrument the moment somebody clicks Cancel
Three signals are enough to make this operable. Emit a counter for cancellation requests by terminal outcome, a histogram from cancel request to reconciliation, and a gauge for imports stuck in cancelling. Log the import ID, batch ID, actor, prior state, resulting HTTP status, and provider request ID when available. Do not log the API key or raw customer media metadata.
Alert on behavior, not one noisy request. A growing count of unreconciled cancellations means operators cannot tell what entered search or moderation review. A sudden cluster of wrong-folder cancellations may point to an ambiguous folder picker, even if every API call succeeds.
The before/after is crisp: before, “cancelled” is a UI event; after, it is a traceable state transition with a bounded cleanup set.
Limits that should change the decision
Cancellation is cooperative. Work completed before the provider accepts the request may remain completed, so recovery must report and reconcile it. Network failure also leaves the caller uncertain; follow the stored batch ID with a status read instead of submitting again.
Choose an orchestrator if you need per-image pause, compensation across several services, or a detailed activity history. Choose the direct batch approach if batch-level stop plus status-based reconciliation matches the product boundary. In either case, validate moderation coverage first, retain the batch ID, and make cleanup narrower than the original import.
Top comments (0)