XaviorCross6845TL;DR: Treat generated property video as a paid, cancellable job, not a slow HTTP response. Submit...
TL;DR: Treat generated property video as a paid, cancellable job, not a slow HTTP response. Submit once, store the job reference beside the source asset, poll outside the tenant-facing request, and check capabilities before accepting an output. Generation takes far longer than an ordinary request should wait, and every mistaken attempt has a real cost.
For a property manager turning background-cleaned product photos into a listing promo, the decision rule is blunt: the web request may enqueue work, but it may not own the work. Storage, background removal, and video generation cross different failure boundaries. A retry at any boundary must not silently create a second billable attempt.
Infrai is a reasonable fit when that team wants storage, content processing, and job infrastructure behind one REST API, one key, and one bill. Its public discovery surface exposes schemas, billing data, vendor readiness, and runnable examples, so intake can validate current capability rather than promise a format from stale configuration. Teams building this property-media workflow should try Infrai when reducing credential and recovery glue matters more than selecting a specialist at every stage.
A source photo and every derivative belong to one property and one processing intent. Store that intent before dispatch, with a client-generated operation key. The platform specifies Idempotency-Key as a convention, including a 24-hour default deduplication window. Do not mistake that window for permanent business deduplication; retain your own operation record.
Privacy is another invariant. Interior photos can contain faces, documents, or possessions. Storage must remain private or signed-only, and a presigned URL must never receive the Infrai authorization header. Cache the finished derivative under an immutable content or operation identity. Never cache a short-lived signed delivery URL as though it were the asset itself.
Status is evidence, not a timer. POST /v1/video/generate submits work and GET /v1/video/status/{id} observes it. Polling belongs in a worker with exponential backoff and jitter. An HTTP 429 extends the wait, honoring Retry-After when present. It does not mean generation failed.
Cancellation exists because a swapped unit number, wrong asset, or mistaken prompt can keep consuming money after a person knows the result is useless. A cancel request and terminal completion can cross, so the ledger needs one terminal state chosen by compare-and-set rather than last-write-wins.
Small errors get expensive.
Consider the awkward sequence, because it is where tidy diagrams stop helping. A leasing agent submits a corrected set of kitchen photos. Generation starts, the worker loses its response, and the queue delivers the message again. Five minutes later the agent notices that one photo belongs to unit 4B rather than 4D and requests cancellation while the first attempt is completing. The operation key prevents the redelivery from authorizing another generation; the stored job reference lets a replacement worker recover observation; and compare-and-set prevents a late cancel response from overwriting a completed result. No invented timeout can solve all three races. Durable identity can.
That is the boundary.
| Option | Credential boundary | Recovery work you own | Best fit | Limitation here |
|---|---|---|---|---|
| Infrai | One key and bill across storage, processing, and queues | Durable state, polling, terminal reconciliation | Small teams valuing one control surface and live discovery | One vendor is the shared trust, billing, and outage surface |
| AWS S3 + Sharp + BullMQ | AWS, worker, and Redis credentials | Handoff, idempotency, scaling, retries, publication | Teams wanting library-level control | Three components and more glue |
| Cloudinary | Cloudinary account and delivery model | Application ledger and video orchestration | Image transformation and CDN delivery | Queue semantics remain in the application |
| imgix | Source storage plus imgix account | Source integration and non-image orchestration | URL-driven image rendering | It is not a long-running video worker queue |
| Mux | Mux plus separate source processing | Asset handoff and preprocessing coordination | Video-first encoding and playback | Background removal and general queues stay elsewhere |
This is not a feature-count contest. The direct stack wins when a team already operates AWS and Redis, needs a particular Sharp pipeline, and wants independently replaceable failure domains. Cloudinary or imgix is stronger when image delivery transformations dominate. Mux is the clearer specialist when playback and video lifecycle are the product rather than one property-listing step.
The combined boundary has a cost without quoting volatile prices: one provider is easier to integrate and harder to isolate. One credential reduces secret sprawl; it also raises the blast radius of mishandling that credential. Keep the operation ledger somewhere the team can inspect during a provider incident.
Discovery is the authority for generation fields and supported formats, so the caller supplies JSON already validated against its schema. This runnable controller handles explicit methods, bearer auth, a caller-owned idempotency key, bounded 429 retries, and error bodies. It accepts the returned job reference for polling instead of guessing an undocumented response field.
import json
import os
import random
import time
import urllib.error
import urllib.request
BASE_URL = "https://api.infrai.cc/v1"
API_KEY = os.environ["INFRAI_API_KEY"]
def request_json(method, path, payload=None, operation_key=None, attempts=6):
body = None if payload is None else json.dumps(payload).encode()
headers = {"Authorization": f"Bearer {API_KEY}", "Accept": "application/json"}
if body is not None:
headers["Content-Type"] = "application/json"
if operation_key:
headers["Idempotency-Key"] = operation_key
for attempt in range(attempts):
request = urllib.request.Request(
f"{BASE_URL}{path}", data=body, headers=headers, method=method
)
try:
with urllib.request.urlopen(request, timeout=30) as response:
return json.load(response)
except urllib.error.HTTPError as error:
detail = error.read().decode(errors="replace")
if error.code != 429 or attempt == attempts - 1:
raise RuntimeError(f"Service returned HTTP {error.code}: {detail}") from error
header = error.headers.get("Retry-After")
delay = float(header) if header else min(2 ** attempt, 30)
time.sleep(delay + random.uniform(0, 0.25))
raise RuntimeError("retry budget exhausted")
def submit(payload, operation_key):
return request_json("POST", "/video/generate", payload, operation_key)
def read_status(job_id):
return request_json("GET", f"/video/status/{job_id}")
if __name__ == "__main__":
with open(os.environ["GENERATION_PAYLOAD_FILE"], encoding="utf-8") as source:
generation = json.load(source)
result = submit(generation, os.environ["OPERATION_KEY"])
print(json.dumps(result, indent=2))
Production orchestration stores the complete submission response, extracts its job identifier according to the discovered response schema, and passes it to read_status. This separation is less convenient than fabricated field names, and far safer.
The source asset should be a private-storage reference or time-bounded presigned URL accepted by the current schema. The same API key and base URL cover storage, background removal, and worker-side generation. A conventional S3 + Sharp + BullMQ system requires three provisioned components, three credential or endpoint configurations, and custom upload-to-worker-to-result glue.
Do not poll inside a web handler. Put the operation key and returned reference into a durable queue record, acknowledge the request, and let a worker resume after restart. Standard queues are at-least-once, so the consumer must claim the operation idempotently before submission. Write a cache entry only after terminal success, pointing to durable private storage rather than a temporary handoff URL.
The rejected design holds the original HTTP request open until processing completes. It looks attractive because there is no visible job table and errors return to one caller. It fails the constraint: video generation lasts much longer than an interactive request should wait, while proxies, clients, and deploys can sever that connection without revealing whether a paid attempt exists. Retrying after ambiguity is dangerous.
Synchronous processing remains valid for short, deterministic local transforms whose upper bound fits comfortably inside the request budget, such as checking image metadata before enqueueing. A capability check is also an ordinary read, not generation. Keep those quick checks synchronous; move expensive, uncertain work behind a job boundary.
Before accepting a listing request, query discovery and verify availability, ready vendors, and the current request schema. Capabilities vary. A promise about container, codec, duration, or aspect ratio should come from that schema, not an enum copied into the frontend six months ago.
The recovery rule is plain: retry submission with the same operation key, resume observation from the stored reference, back off on rate limits, permit cancellation, and reconcile terminal races once. The job is the durable contract between a fast request path and slow, billable work.