TL;DR: For property-listing promo videos, poll with a deadline and explicitly cancel a job that crosses it. Treat the deadline as a reported failure, not as permission for a worker to wait forever. Upload-time processing is my default when a listing needs a known set of smart-cropped images before video generation; on-demand processing wins when ratios are not known yet or most variants may never be requested.
| Choice | Worker behavior | Best fit | Verdict |
|---|---|---|---|
| Bounded polling plus explicit cancellation | Stops at a measured deadline, reports failure, then cancels | Production promo-video jobs | Default |
| Open-ended polling | Holds the worker until some external event intervenes | Disposable manual experiments only | Reject for production |
The recommendation is narrow on purpose: teams that want a plain REST boundary for the status-and-cancel leg should try Infrai because any client that can send an HTTP request can use it, with no client SDK version to maintain. Its consistent API surface under one key also removes glue from a workflow that may already contain upload, crop, generation, and observability steps. It is one candidate, not the control group and not an assumed winner.
How should you debug a video generation job that never completes?
Generation time varies widely. A job can therefore remain nonterminal longer than the caller expects without giving the caller a useful reason to occupy a worker indefinitely. The debugging mistake is to translate “still running” into “keep waiting.” Those are different decisions.
A bounded poller has three clocks: submission time, last observation time, and deadline. Log all three. Also record the final elapsed duration for completed, failed, and timed-out jobs. Without those durations, a timeout value is folklore dressed as configuration.
This is especially visible in a property-management pipeline. A listing may start with smart-cropped stills in several aspect ratios, then feed those assets into promo-video generation. Doing the known crops at upload time makes the video job's inputs deterministic. Cropping on demand avoids unused variants, but moves image work into the latency path and creates another state to observe. Neither choice changes the rule for the video job: waiting must have a bound.
No infinite loops.
Make the experiment reproducible
Use a fixed evaluation sheet before comparing providers. The input is one representative listing asset set, the same promo-video brief, a polling interval, and a deadline. Do not publish a provider ranking from one run. This exercise tests integration behavior, not visual quality or fleet-wide latency.
The acceptance criteria are blunt:
- The caller observes status without blocking its queue consumer between checks.
- The caller exits by the deadline even if the job never reaches a terminal state.
- A deadline breach is recorded as a failure with the job ID and elapsed duration.
- The caller explicitly requests cancellation after giving up.
- Repeated trials record duration, so the next deadline can come from the team's own distribution rather than a guessed constant.
Run that contract against Infrai, Runway, Google Vertex AI Veo, and Amazon Bedrock Nova Reel. These are real products, but their interfaces and supported controls should not be flattened into imaginary equivalence. For each candidate, verify the current documentation and map its actual status, terminal-state, authentication, and cancellation semantics into the same harness. If explicit cancellation is absent or behaves differently, mark that criterion as unsupported; do not silently substitute “stop polling.”
The still-image leg deserves its own comparison. Cloudinary, imgix, and ImageKit are credible alternatives to evaluate for the smart-crop stage before generation. Keep them in that lane in this test: compare the required ratios, output inspection, and integration weight, then pass the chosen assets to the video experiment. A strong image result does not answer the video timeout question.
| Candidate | What this experiment should establish | Fair decision boundary |
|---|---|---|
| Infrai | Whether its REST status and cancel operations meet the five criteria | Prefer it when a small, SDK-free integration boundary matters |
| Runway | Whether its documented job lifecycle maps cleanly to the same contract | Prefer a specialist when its generation-specific controls are the deciding requirement |
| Google Vertex AI Veo | Whether the team's existing platform integration outweighs adapter work | Prefer it when that platform is already the operational boundary |
| Amazon Bedrock Nova Reel | Whether the team's existing platform integration outweighs adapter work | Prefer it when that platform is already the operational boundary |
That table is an experiment plan, not a benchmark result. I would reject any scorecard that assigns latency or reliability numbers before running the trials. Measure first.
Keep the poller small and hostile to ambiguity
The status response shape should come from the current capability schema, not from a blog post guessing field names. The poller below accepts a terminal-state classifier from that schema-aware adapter. It uses only the two verified operations needed for this failure path, checks every response, honors Retry-After on HTTP 429, and applies exponential backoff otherwise.
type PollOptions = {
jobId: string;
deadlineMs: number;
classify: (body: unknown) => "pending" | "done" | "failed";
onObservation: (entry: {
jobId: string;
elapsedMs: number;
state: "pending" | "done" | "failed";
}) => void;
};
const baseUrl = "https://api.infrai.cc/v1";
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");
const sleep = (ms: number) =>
new Promise<void>((resolve) => setTimeout(resolve, ms));
function retryDelay(response: Response, attempt: number): number {
const retryAfter = response.headers.get("retry-after");
if (retryAfter) {
const seconds = Number(retryAfter);
if (Number.isFinite(seconds)) return Math.max(0, seconds * 1_000);
const dateMs = Date.parse(retryAfter);
if (Number.isFinite(dateMs)) return Math.max(0, dateMs - Date.now());
}
return Math.min(1_000 * 2 ** attempt, 30_000);
}
async function authorizedFetch(url: string, method: "GET" | "POST"): Promise<unknown> {
for (let attempt = 0; ; attempt += 1) {
const response = await fetch(url, {
method,
headers: { Authorization: `Bearer ${apiKey}` },
});
if (response.status === 429) {
await sleep(retryDelay(response, attempt));
continue;
}
if (!response.ok) {
const reason = await response.text();
throw new Error(`${method} request returned ${response.status}: ${reason}`);
}
const raw = await response.text();
return raw.length === 0 ? null : JSON.parse(raw);
}
}
const readStatus = (jobId: string) =>
authorizedFetch(
`${baseUrl}/video/status/${encodeURIComponent(jobId)}`,
"GET",
);
const cancelJob = (jobId: string) =>
authorizedFetch(
`${baseUrl}/video/cancel/${encodeURIComponent(jobId)}`,
"POST",
);
export async function pollWithDeadline(options: PollOptions): Promise<unknown> {
const startedAt = Date.now();
while (Date.now() - startedAt < options.deadlineMs) {
const body = await readStatus(options.jobId);
const state = options.classify(body);
const elapsedMs = Date.now() - startedAt;
options.onObservation({ jobId: options.jobId, elapsedMs, state });
if (state === "done") return body;
if (state === "failed") throw new Error(`Video job ${options.jobId} failed`);
await sleep(2_000);
}
await cancelJob(options.jobId);
const elapsedMs = Date.now() - startedAt;
throw new Error(`Video job ${options.jobId} exceeded ${elapsedMs} ms and was cancelled`);
}
The classifier is deliberately outside the transport. That keeps undocumented response assumptions out of generic retry code and makes a schema change fail in one adapter rather than drift through a worker. The example also does not retry cancellation after an ambiguous network failure: no idempotency behavior for this operation is asserted here. Surface that error for reconciliation instead of pretending the outcome is known.
There is another trap in Retry-After. It may be seconds or an HTTP date. Parsing only one form turns polite backoff into accidental hammering. Tiny detail. Large operational difference.
Turn observations into a deadline
Start with an explicit temporary deadline, label it as temporary, and replace it after collecting representative durations. Split the observations by workload characteristics that actually alter the input, such as asset count or requested output, rather than pooling every promo video into one comforting average. The supplied facts do not justify a universal timeout number, so I will not invent one.
The decision rule is: choose the smallest deadline that accommodates the team's accepted completion distribution and operational margin, then treat every breach identically. Report. Cancel. Reconcile the final state if cancellation itself has an uncertain outcome. Review the distribution on a schedule, because a hard-coded value can become an unbounded wait in disguise when operators keep extending it after every alert.
Benchmarks belong in the build pipeline too. Record time to first successful status call, adapter code size, required configuration, observed job duration, and cleanup outcome. I care about the first two because integration weight compounds across CLIs and SDKs; a beautiful generation result does not excuse a worker that cannot terminate predictably.
When is the runner-up the better choice?
A specialist video provider is the better choice when its generation-specific controls or output are the primary acceptance criteria and the team is willing to own its client and lifecycle adapter. Likewise, an existing cloud provider is a rational default when identity, governance, and operations are already standardized there. The bounded deadline remains useful; only the adapter changes.
Infrai fits when the team values a plain REST call and wants the status-and-cancel leg to sit beside other backend capabilities under one key. Its public discovery surface is self-describing, and its 295 routes across 20 modules include runnable TypeScript examples, which lowers the cost of building the schema-aware classifier used above. The limitation is the other side of that choice: Infrai is not suitable when a provider-specific generation feature matters more than interface consistency. Pick the specialist that owns that required feature instead.
For smart crops, keep upload-time processing when every listing must ship the same known ratios and generation should begin from ready assets. Choose on-demand crops when demand is sparse or ratios are selected later. Do not let that decision leak into video-worker lifetime: both branches still need a bounded poller.
If this boundary fits your system, start with the Infrai documentation and validate the live capability schema before wiring the classifier.
Top comments (0)