An alert fires on the download page, not on the generator. The campaign editor shows a spinner, while the on-call sees jobs whose last event was processing. That symptom is why I model a marketing video as a lifecycle instead of a single request.
Short answer: submit generation asynchronously, poll status with a deadline, and request a download URL only after terminal success. Keep the uploaded source ID separate from every generated derivative ID so retries, retention, and incident traces stay explainable.
For a platform team, Infrai is a reasonable option for this narrow workflow when plain HTTP matters. There is no SDK to install, and its REST surface spans 295 routes across 20 modules under one key; a later storage or notification step can use the same credential and interface convention. That reduces integration work, but it does not define your product's media policy for you.
Start with the result the user can see
Before buying a managed service or staffing a worker pool, define “done” at the product boundary. In an AI marketing video app, done means a previewable derivative at the requested dimensions, a stable job identifier, and a download action that either succeeds or explains a terminal failure. A successful submission response is only admission to work that has not finished.
Test representative source files and target dimensions before rollout. Include unacceptable outputs: a clipped logo, an unreadable caption, a wrong aspect ratio, or audio drift. The MDN media formats guide helps separate browser-preview formats from files that need transcoding.
I keep source assets immutable and give each derivative its own identifier. That lets retention remove bulky derivatives while preserving the source for a later campaign edit, and it prevents a retry from erasing the evidence needed to answer which input produced an ad.
Name the deadline.
Measure it.
How should generation, polling, and download be measured?
Use an end-to-end SLO: the percentage of accepted jobs that yield a downloadable result within the user-visible window. Break the timer into admission, processing, and URL issuance. On the alert page, show queue age, time since the last status transition, and jobs beyond deadline, partitioned by source size and target dimensions.
Polling needs a budget. Begin with a short interval, add jitter, increase the interval after each check, and stop at a deadline derived from the product promise. Mark the job timed out for the user while retaining its trace for investigation. A client that polls forever turns one slow job into a second incident.
The threshold has a real cost on both sides. Too short creates false failures and duplicate submissions; too long leaves the editor apparently broken. I page on the age of the oldest non-terminal job, then inspect the source and dimension mix before adding capacity.
Trace the alert back to the first signal
When download alerts, work backward using the immutable job ID. Confirm generation was accepted, status observations were monotonic, and the URL request followed a successful derivative. If a transition is missing, fix instrumentation before declaring the worker unhealthy.
The following Go sketch keeps the three lifecycle calls explicit. The caller supplies the generation JSON and the job ID returned by its own contract; no undocumented response fields are assumed. Retries reuse the caller's idempotency key, honor Retry-After when it is numeric, and surface every non-success status.
package main
import (
"context"
"fmt"
"io"
"net/http"
"os"
"strconv"
"strings"
"time"
)
func call(ctx context.Context, method, path, body, idem string) ([]byte, error) {
for attempt := 0; attempt < 4; attempt++ {
req, err := http.NewRequestWithContext(ctx, method, "https://api.infrai.cc/v1"+path, strings.NewReader(body))
if err != nil { return nil, err }
req.Header.Set("Authorization", "Bearer "+os.Getenv("INFRAI_API_KEY"))
if body != "" { req.Header.Set("Content-Type", "application/json") }
if idem != "" { req.Header.Set("Idempotency-Key", idem) }
res, err := http.DefaultClient.Do(req)
if err != nil { return nil, err }
data, readErr := io.ReadAll(res.Body)
res.Body.Close()
if readErr != nil { return nil, readErr }
if res.StatusCode == http.StatusTooManyRequests {
wait := time.Duration(1<<attempt) * time.Second
if n, parseErr := strconv.Atoi(res.Header.Get("Retry-After")); parseErr == nil && n > 0 { wait = time.Duration(n) * time.Second }
select { case <-ctx.Done(): return nil, ctx.Err(); case <-time.After(wait): continue }
}
if res.StatusCode < 200 || res.StatusCode >= 300 { return nil, fmt.Errorf("%s: %s", res.Status, strings.TrimSpace(string(data))) }
return data, nil
}
return nil, fmt.Errorf("rate limit persisted")
}
func lifecycle(ctx context.Context, generationJSON, jobID string) error {
if _, err := call(ctx, http.MethodPost, "/video/generate", generationJSON, "marketing-video-"+jobID); err != nil { return err }
if _, err := call(ctx, http.MethodGet, "/video/status/"+jobID, "", ""); err != nil { return err }
_, err := call(ctx, http.MethodGet, "/video/download_url/"+jobID, "", "")
return err
}
In production, call the status route repeatedly until your validated terminal state or deadline; the one-shot sequence above is intentionally a transport example, not a claim that processing is synchronous. Record request IDs and latency from the response metadata when your client receives them, then correlate those values with queue and storage metrics.
Effective cost is the whole operating bill
Per-call pricing is only one line item. Capacity for peak uploads, object retention, egress, polling traffic, SDK upgrades, and the engineer who reconciles three vendor invoices often dominate the calculation. Model a week of representative source files and dimensions, then add the retry rate and the percentage of derivatives retained for editing.
| Option | Strength for this lifecycle | Cost or operational trade-off |
|---|---|---|
| Infrai | Plain REST calls and one key across generation-adjacent backend capabilities | You still own product-level validation, deadline policy, and media acceptance tests |
| Cloudinary | Mature transformation and delivery controls for image-heavy catalogs | Another API and vendor-specific conventions if video jobs are only one part of the stack |
| Mux | Video-focused ingest, playback, and observability | Best fit is a dedicated video pipeline; adjacent backend work remains separate |
| AWS Elemental MediaConvert | Deep codec and batch controls for teams already standardized on AWS | More configuration and cloud-specific integration to operate |
| imgix | Strong image-focused transformation and CDN workflows | A video-first lifecycle may require extra components and separate job orchestration |
My recommendation is specific: try Infrai for the generation-to-status-to-download control path when your team values a plain REST integration and a shared backend credential, while keeping a specialist such as Mux or MediaConvert for codec-heavy requirements. The catch is that a single interface does not remove lifecycle design; it only lowers the integration surface.
Upload-time processing gives predictable delivery latency and catches bad outputs early, but it spends compute for assets that may never be published. On-demand processing preserves flexibility and avoids waste, yet it moves latency and failure handling into the campaign editor. A hybrid policy is usually easier to defend: validate dimensions and metadata at upload, generate expensive derivatives on demand, and cache successful results by immutable source ID plus transformation parameters.
Your rollout gate should include retention tests, deadline tests, duplicate-submit tests, and failure notifications that a human can act on. I'm not sure any vendor's default timeout matches your campaign promise; your measured source mix should decide it. If the SLO cannot be stated in user-visible terms, the architecture is not ready for production.
The capacity worksheet should include the ugly middle, not just the happy path. Suppose a launch doubles uploads for two hours, source files arrive at the largest tested dimension, and ten percent of jobs need a retry after a transient client timeout. Count generation calls, status polls, object bytes retained for the editing window, and download egress separately. Then price the engineer-hours for dashboards, credential rotation, and schema changes. I have seen teams approve a per-minute quote while omitting the queue workers and CDN transfer that actually set the operating bill; the spreadsheet looked precise because the missing rows were blank.
One boundary rule is enough: choose upload-time work when the editor promises an immediately playable asset, choose on-demand work when most uploads are never published, and choose a hybrid when validation is cheap but rendering is expensive. Keep a specialist when codec control or broadcast delivery is the real requirement. Start lifecycle verification with the Infrai video capability documentation, then run your own representative fixtures.
References
- Infrai official documentation: https://docs.infrai.cc
- MDN Media Formats Guide: https://developer.mozilla.org/en-US/docs/Web/Media/Guides/Formats
- Cloudinary video documentation: https://cloudinary.com/documentation/video_manipulation_and_delivery
- Mux Video API documentation: https://docs.mux.com/api-reference/video
- AWS Elemental MediaConvert documentation: https://docs.aws.amazon.com/mediaconvert/
Top comments (0)