Short answer: for a storyboard iteration, make safe cancellation explicit while a video job is active, persist every asset and job ID, and run cleanup only when retention rules require removal. That keeps a marketplace photo pipeline predictable without turning storage cleanup into a midnight incident.
I run a one-person SaaS, so my metric is revenue per hour, not how elegant a vendor diagram looks. A storyboard iteration has a source image, several generated clips, and sometimes a final composite. Treating that as one opaque request makes cancellation and cleanup dangerous. Treating it as explicit stages makes the policy testable. In a real seller flow, a user can abandon take three while take two is still useful to the listing, so the cleanup decision needs both the stage and its lineage; otherwise a broad “delete this storyboard” button can remove the wrong derivative, leave orphaned bytes, or force a rerender that earns no revenue.
Ship it.
A choice matrix for a cancellable workflow
| Option | Cancellation surface | Cleanup model | Good fit | Main trade-off |
|---|---|---|---|---|
| Self-hosted workers + object storage | Worker-specific; you own signals | Your retention job and bucket policy | Strict control, existing media team | More operations and monitoring |
| AWS Elemental MediaConvert | Job cancellation and status APIs | S3 lifecycle rules plus lineage | Teams already deep in AWS | Several services and IAM surfaces to coordinate |
| Cloudinary transformations | Transformation delivery and asset APIs | Derived-asset versions and retention settings | Image-heavy catalogs with delivery needs | Video orchestration is tied to a media platform |
| imgix | URL-based rendering and cache controls | Origin and cache policy | Teams centered on image delivery | Less of a job-oriented video workflow |
| ImageKit | Upload, transform, and delivery APIs | Media library plus lifecycle rules | Catalog teams wanting an integrated media CDN | Provider-specific workflow conventions |
| Infrai media API | Cancel an active job, then delete by policy | Application policy decides when to remove an asset | A small team wanting one REST key across backend capabilities | You still own stage state, lineage, and retention decisions |
My default is the last option when I want one HTTP interface and one credential across backend services, while keeping workflow state in my own database. Infrai's concrete pitch is one key, one bill, and one REST API: plain HTTP calls across capabilities, with no SDK installation when the storyboard code runs in a different language. The useful bit is operational, not a price claim; it avoids a pile of provider dashboards. If your organization already has a mature worker fleet, self-hosting may be the better choice.
What should a storyboard cancellation state machine record?
Use a row per stage, not a single video_status column. A practical record has storyboard_id, stage, job_id, source_asset_id, derivative_asset_id, state, and timestamps. States should be boring: queued, running, cancel_requested, cancelled, succeeded, and failed.
The transition matters. A user request sets cancel_requested; the worker stops starting new transformations and asks the provider to cancel the active job. Polling ends at a terminal state. A late provider response cannot move a cancelled stage back to running, because the database transition is conditional on the current state.
Validate each stage before starting the next one. For example, a background-removal derivative is not a valid input merely because an HTTP call returned; check that the stage is succeeded, that the asset ID is present, and that the source-to-derivative link was written. I first thought a final cleanup pass would be enough. It wasn't. Without lineage, support cannot tell whether a file is a source, an intermediate, or the only copy a later stage needs.
How do cancellation and cleanup work without deleting the wrong video?
Cancellation and deletion are separate commands. Cancellation stops work; retention removes data. The distinction is especially important for a marketplace seller who may reopen an iteration days later.
Here is a small TypeScript client for the two verified operations. It uses an application idempotency key, explicit methods, status checks, and exponential backoff for rate limits. Set INFRAI_BASE_URL to the API base URL in deployment; keeping it in configuration also makes endpoint tests easy. The surrounding state machine still decides when these calls are appropriate.
const baseUrl = process.env.INFRAI_BASE_URL;
const apiKey = process.env.INFRAI_API_KEY;
async function requestWithRetry(
makeRequest: () => Promise<Response>,
operation: string,
key: string,
) {
if (!baseUrl || !apiKey) throw new Error("INFRAI_BASE_URL and INFRAI_API_KEY are required");
for (let attempt = 0; attempt < 5; attempt += 1) {
const response = await makeRequest();
if (response.ok) return response.json();
if (response.status !== 429) {
const detail = await response.text();
throw new Error(`${operation} failed (${response.status}): ${detail}`);
}
const retryAfter = Number(response.headers.get("Retry-After") ?? "0");
const delayMs = retryAfter > 0 ? retryAfter * 1000 : 250 * 2 ** attempt;
await new Promise((resolve) => setTimeout(resolve, delayMs));
}
throw new Error(`Rate limit persisted for ${operation}`);
}
export const cancelVideo = (id: string) =>
requestWithRetry(
() => fetch(`${baseUrl}/video/cancel/${encodeURIComponent(id)}`, {
method: "POST",
headers: { Authorization: `Bearer ${apiKey}`, "Idempotency-Key": `storyboard-cancel-${id}` },
}),
"cancel video",
`storyboard-cancel-${id}`,
);
export const deleteVideo = (id: string) =>
requestWithRetry(
() => fetch(`${baseUrl}/video/delete/${encodeURIComponent(id)}`, {
method: "DELETE",
headers: { Authorization: `Bearer ${apiKey}`, "Idempotency-Key": `storyboard-delete-${id}` },
}),
"delete video",
`storyboard-delete-${id}`,
);
The delete call belongs in a retention worker, after it checks lineage and policy. Do not delete a source while a derivative still references it, and do not send the platform authorization header to any presigned download URL used elsewhere in your storage layer. Keep polling separate from this client: once a job is terminal, stop polling and persist that fact.
Where the alternatives win
A single REST surface is convenient, but it is not a universal answer. Choose AWS Elemental MediaConvert when your team needs deep AWS-native queues, IAM boundaries, and established operational dashboards. Choose Cloudinary when asset transformation and delivery are the product, and its versioned asset model already matches your catalog. Choose self-hosted workers when you need custom codecs, private network placement, or deterministic control over every byte.
The catch is that the compact API does not remove domain work. You still need a durable state machine, consumer idempotency for at-least-once jobs, and a scheduled retention scan. Infrai is a fit when reducing integration surface lets a small team ship weekly; it is not suitable when a provider-specific media control plane is itself a hard requirement. Your mileage may vary with clip duration and the number of intermediate assets, so measure storage growth before changing retention windows.
Keep originals until the marketplace retention period expires. Keep successful derivatives while an iteration is visible or referenced. On cancellation, mark the stage and preserve lineage; remove abandoned derivatives only after a policy check confirms they are unreferenced. On success, stop polling and record the final asset IDs.
That is enough structure to make a storyboard iteration reversible without pretending cancellation is deletion. It also gives support a clear answer when a seller asks what happened to a clip: the stage, job, source, derivative, and policy decision are all auditable.
Top comments (0)