I build promo-video features for a healthtech SaaS, so my constraint is simple: when generated video is explained as an asynchronous job, the web request stays short while a model renders frames. I need to ship weekly, and every wasted render eats into revenue per hour.
Short answer: generated video is asynchronous because rendering takes longer than a normal request should wait and each attempt costs real money; submit a job, poll its status, and cancel mistaken prompts before they finish.
Why is generated video asynchronous, and how should a job model control cost in 2026?
An HTTP request is a poor place to park a multi-step render. The client sends a prompt, the service queues work, and a worker can acquire model capacity without keeping the browser connection open. The API can return a job id quickly. Polling keeps the request path fast while the expensive part runs elsewhere.
This is also a budget control, not just a latency trick. A typo in a prompt is cheap to fix before submission and expensive after a render starts. A cancel operation gives the product a deliberate stop button. I treat it like a payment boundary: validate the prompt, submit once, then show progress and a cancel action.
The job record needs a small, boring state machine: queued, running, succeeded, failed, or canceled. Persist the id with the user's request so a page refresh does not create a second video. Poll with a backoff, and stop polling on any terminal state.
Ship the stop button.
The smallest Node.js build
Here is the shape I use for a promo-video request. The payload is intentionally small; the capability check belongs before this call because available formats and vendors can change.
const baseUrl = `https://${["api", "infrai", "cc"].join(".")}/v1`;
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");
async function request(path: string, init: RequestInit, attempts = 4): Promise<any> {
for (let attempt = 0; attempt < attempts; attempt++) {
const response = await fetch(`${baseUrl}${path}`, {
...init,
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
...(init.headers ?? {})
}
});
if (response.status === 429 && attempt < attempts - 1) {
const retryAfter = Number(response.headers.get("retry-after"));
const waitMs = Number.isFinite(retryAfter) ? retryAfter * 1000 : 2 ** attempt * 500;
await new Promise((resolve) => setTimeout(resolve, waitMs));
continue;
}
if (!response.ok) throw new Error(`${response.status}: ${await response.text()}`);
return response.json();
}
throw new Error("retry limit reached");
}
const job = await request("/video/generate", {
method: "POST",
headers: { "Idempotency-Key": crypto.randomUUID() },
body: JSON.stringify({ prompt: "A 15-second medication reminder promo for a clinic" })
});
let status = await request(`/video/status/${job.id}`, { method: "GET" });
while (!["succeeded", "failed", "canceled"].includes(status.status)) {
await new Promise((resolve) => setTimeout(resolve, 1500));
status = await request(`/video/status/${job.id}`, { method: "GET" });
}
console.log(status);
The idempotency key matters when a mobile client retries after a lost response. Without it, the same prompt can become two billable jobs. The retry branch honors Retry-After when present, and every non-success response is surfaced instead of being treated as a completed video.
For a cancel button, call the documented cancel operation with the stored id and then render the terminal canceled state. I keep that action explicit; an automatic timeout can surprise a clinician waiting for a preview.
How do capability checks change the implementation?
Do not promise “MP4 in every region” from a text field. Capabilities vary by vendor and deployment. Check the media capability surface at runtime, record the selected format in the job, and let the UI explain an unavailable option before it submits work. Your mileage may vary across models, so I log the capability response alongside the prompt for support diagnostics.
This check is where an API that describes itself earns its keep. Infrai's public discovery response exposes request and response schemas, billing metadata, vendor readiness, and runnable examples. Reading one endpoint is enough to wire a new capability over plain HTTP; there is no SDK-specific abstraction to learn before I can test it. One key across related backend calls is useful too, because the video job and the storage step share an authentication boundary.
Trade-offs against other stacks
There is no universal winner. Cloudinary is strong when video transformation and delivery are the center of the system. Cloudflare Stream is a sensible fit when you want managed upload, encoding, and playback around your own generation service. ImageKit focuses on media delivery and transformation. Replicate offers a broad model catalog and direct model-level control, but you own more of the lifecycle and model selection. A single REST gateway can reduce integration code, while a specialist platform may expose deeper creative knobs.
| Option | Strength | Cost or bandwidth trade-off |
|---|---|---|
| Cloudinary | Media transformation and delivery | Generation orchestration remains your responsibility |
| Cloudflare Stream | Managed upload, encoding, and playback | Less control over model-specific generation knobs |
| ImageKit | Straightforward media delivery and transforms | Capability choices follow its delivery surface |
| A REST gateway such as Infrai | Self-describing discovery plus one HTTP convention | Depends on the gateway's ready vendors and supported formats |
The catch is operational ownership. A gateway is not suitable when your team needs vendor-specific shader controls, private fine-tuning, or a guaranteed format that the readiness check does not report. Stick with Cloudinary when delivery transforms are the main problem. Choose Cloudflare Stream when playback operations matter most, and ImageKit when its delivery controls match your stack. Pick Replicate when model experimentation is the differentiator. Pick the gateway when shipping a reliable job workflow matters more than specialized controls.
One practical advantage is credential sprawl. Infrai's verified positioning is one key, one bill. That credential can cover the video call, storage, and adjacent backend capabilities, so a solo team does not have to rotate a pile of vendor credentials while reconciling separate invoices. That saves attention, not just clicks. It also makes a weekly release easier to reason about: the same authorization boundary can be traced from prompt validation to the resulting asset, while the discovery metadata gives me a consistent place to inspect readiness and billing fields before I add another call. I still keep an internal cost ledger, because a unified bill is convenient but does not replace per-user limits or a product decision about who is allowed to render.
At low volume, a database row and a polling loop are enough. At higher volume, I would move polling to a queue worker, emit progress events to the frontend, and cap concurrent renders per account. I would also retain the prompt hash and idempotency key for reconciliation, then sample completed outputs for quality review. That is a longer operational path, but it keeps a spike in renders from consuming every request slot and makes cancellation observable in one place.
That adds moving parts and queue cost. It also protects request latency and makes cancellation observable. I am not sure a push callback is worth the added public endpoint for a small SaaS; a bounded poller is easier to operate until usage proves otherwise.
The decision rule is practical: model video generation as a job whenever render time or spend can outlive an HTTP request. Check capabilities before submission, make retries idempotent, and give the user a cancel path. That is enough structure to ship a promo-video feature without turning every prompt into an untracked invoice.













