Async Video Generation API — How to Poll for Results (2026)

Aug 19, 2026·8 min read

You POST to a video generation endpoint, wait for a response, and get a 504 Gateway Timeout after 30 seconds. Your load balancer cut the connection before the model finished. Video generation takes 30 seconds on the fast end and 3–5 minutes for high-resolution outputs — far longer than any HTTP timeout your infrastructure will tolerate.

Every video generation API on GenRelay uses an async job pattern: you submit a job and receive a job ID immediately, then poll a separate status endpoint until the job reaches completed or failed. This guide explains how to implement that pattern reliably for Veo 3.1, Grok Imagine, and Gemini Omni Flash.


Why do video APIs use async job patterns?

Direct answer: video generation takes too long for synchronous HTTP requests to survive.

A typical generation timeline:

  • Grok Imagine 1.0 (t2v): 30–60 seconds
  • Gemini Omni Flash (720p): 45–90 seconds
  • Veo 3.1 Lite (720p): 60–120 seconds
  • Veo 3.1 Lite (1080p): 90–180 seconds

Standard HTTP client and proxy timeouts sit at 30–60 seconds. Even if your client timeout is generous, most cloud load balancers and API gateways enforce a maximum connection lifetime that video generation will routinely exceed.

The async pattern solves this cleanly: the initial POST returns a 202 Accepted with a job ID in under one second. You poll a GET /v1/videos/jobs/{id} endpoint on a timer until the status flips to completed, then retrieve the video URL from the response.


How do I submit a video generation job?

The endpoint is POST /v1/videos/generations. Required fields: model and prompt. Optional but important: duration, resolution, and aspect_ratio.

import os
import requests

API_KEY = os.environ["GENRELAY_API_KEY"]

headers = {
    "Authorization": f"Bearer {API_KEY}",
    "Content-Type": "application/json",
}

# Submit the job
response = requests.post(
    "https://genrelay.ai/v1/videos/generations",
    headers=headers,
    json={
        "model": "veo-3.1-lite",
        "prompt": "A timelapse of storm clouds forming over a mountain range, cinematic, 4K",
        "duration": 8,
        "resolution": "720p",
        "aspect_ratio": "16:9",
    },
)
response.raise_for_status()

job = response.json()
job_id = job["id"]
print(f"Job submitted: {job_id}")
print(f"Estimated wait: {job.get('estimated_seconds', 'unknown')}s")

The response body includes id (the job ID you'll poll with) and, where available, an estimated_seconds field that helps you set an appropriate initial wait before your first poll.


How do I write a polling loop that doesn't break?

A polling loop has three failure modes you need to handle explicitly:
1. Premature polling — hammering the status endpoint wastes quota and adds noise
2. Infinite loops — a hung job will poll forever without a timeout ceiling
3. Silent failures — status failed needs an explicit check, not just "not completed yet"

Here is a production-ready polling function:

import time

def poll_video_job(job_id: str, poll_interval: int = 10, timeout: int = 600) -> str:
    """
    Poll until job completes. Returns video URL.
    Raises TimeoutError after `timeout` seconds.
    Raises RuntimeError on job failure.
    """
    status_url = f"https://genrelay.ai/v1/videos/jobs/{job_id}"
    deadline = time.time() + timeout
    elapsed = 0

    while time.time() < deadline:
        resp = requests.get(status_url, headers=headers)
        resp.raise_for_status()
        data = resp.json()
        status = data["status"]

        print(f"[{elapsed}s] Status: {status}")

        if status == "completed":
            return data["output"]["url"]
        if status == "failed":
            raise RuntimeError(f"Job {job_id} failed: {data.get('error', 'unknown error')}")

        time.sleep(poll_interval)
        elapsed += poll_interval

    raise TimeoutError(f"Job {job_id} did not complete within {timeout}s")


# Full flow
job_id = "YOUR_JOB_ID"
try:
    video_url = poll_video_job(job_id, poll_interval=15, timeout=300)
    print(f"Video ready: {video_url}")
except TimeoutError as e:
    print(f"Timed out: {e}")
except RuntimeError as e:
    print(f"Job failed: {e}")

Key decisions in this implementation:

  • poll_interval: how often to check. Set this based on the model's typical generation time — polling every 2 seconds for a job that takes 2 minutes is wasteful.
  • timeout: the ceiling beyond which you give up. Set this to 2–3× the model's worst-case generation time.
  • Status values: pending, processing, completed, failed. Only completed gives you a URL; failed gives you an error detail.

What poll interval should I use for each model?

Different models have different generation speeds. Using the same poll interval for all models either wastes requests (polling too fast) or delays delivery to your users (polling too slow).

Model Typical generation time Recommended first wait Poll interval Max timeout
Grok Imagine 1.0 30–60s 25s 10s 120s
Grok Imagine 1.5 (i2v) 45–90s 35s 15s 180s
Gemini Omni Flash (720p) 45–90s 40s 15s 180s
Gemini Omni Flash (1080p) 60–120s 50s 20s 240s
Veo 3.1 Lite (720p) 60–120s 55s 15s 300s
Veo 3.1 Lite (1080p) 90–180s 80s 20s 360s

A practical approach: sleep for the "first wait" duration before making any poll request at all, then poll on the recommended interval until done. This eliminates early-round wasted requests.

import time

def smart_poll(job_id: str, model: str) -> str:
    model_config = {
        "grok-imagine-1.0": {"first_wait": 25, "interval": 10, "timeout": 120},
        "grok-imagine-1.5": {"first_wait": 35, "interval": 15, "timeout": 180},
        "omni-flash":        {"first_wait": 40, "interval": 15, "timeout": 240},
        "veo-3.1-lite":      {"first_wait": 55, "interval": 15, "timeout": 300},
    }
    cfg = model_config.get(model, {"first_wait": 30, "interval": 15, "timeout": 300})

    print(f"Waiting {cfg['first_wait']}s before first poll...")
    time.sleep(cfg["first_wait"])

    return poll_video_job(job_id, poll_interval=cfg["interval"], timeout=cfg["timeout"])

How do I handle failures and partial results gracefully?

Three scenarios to handle explicitly:

1. Job fails mid-generation

The status field becomes "failed" and data["error"] contains a reason string. Common failure reasons: prompt safety filter triggered, upstream model capacity exhausted, or unsupported parameter combination. Check the error message before deciding whether to retry.

2. Job times out in your polling loop

This doesn't mean the job failed — the model may still be working. You can resume polling later using the same job_id. Store job IDs in your database when you submit, so you can re-poll any job that your process lost track of due to a crash or restart.

3. Network error during polling

The requests.get() call itself can fail. Wrap the poll call in a try-except that catches connection errors and retries with a short backoff:

import requests
from requests.exceptions import RequestException

def safe_get_status(job_id: str) -> dict:
    for attempt in range(3):
        try:
            resp = requests.get(
                f"https://genrelay.ai/v1/videos/jobs/{job_id}",
                headers=headers,
                timeout=10,
            )
            resp.raise_for_status()
            return resp.json()
        except RequestException as e:
            if attempt == 2:
                raise
            time.sleep(2 ** attempt)

What does async video generation cost per model?

Billing is charged when the job completes, not when it's submitted. A failed job is not billed.

Model 720p price 1080p price Billing unit
Veo 3.1 Lite $0.060/s $0.120/s Per second of output
Grok Imagine 1.5 $0.022/s $0.022/s Per second of output
Grok Imagine 1.0 $0.010/s Per second of output
Gemini Omni Flash $0.10 $0.15 Per generation (flat)

Example: 8-second video at different models and resolutions

Model Resolution Cost per video
Grok Imagine 1.0 (t2v) $0.080
Gemini Omni Flash 720p $0.100
Grok Imagine 1.5 720p $0.176
Gemini Omni Flash 1080p $0.150
Veo 3.1 Lite 720p $0.480
Veo 3.1 Lite 1080p $0.960

Omni Flash's flat-rate billing makes it predictable for SaaS products where you want per-generation pricing. Veo 3.1 Lite's per-second billing means longer videos cost proportionally more — account for this if users can choose duration. See the Veo 3 API pricing breakdown for more detailed workload math.


FAQ

Can I cancel a running video job?
Yes. Send DELETE /v1/videos/jobs/{id} to cancel a job that hasn't completed. Cancelled jobs are not billed.

What happens if I lose my job ID?
You can list recent jobs via GET /v1/videos/jobs?limit=20. Jobs are retained for 7 days in your account history.

Is there a webhook option instead of polling?
GenRelay supports webhook callbacks for video jobs. Set the webhook_url field in your generation request and GenRelay will POST the completed job data to your endpoint when done — no polling required. See the Veo 3.1 integration guide at /blog/veo-3-1-api-tutorial for the webhook payload schema.

Are video URLs permanent?
Generated video URLs expire after 24 hours. Download the video file to your own storage (S3, Cloudflare R2) after the job completes. The output.url field in the completed job response is a direct download link.

What's the rate limit on video generation jobs?
Concurrent job limits depend on your plan. On the $14/mo plan, you can run multiple jobs in parallel. If you hit a 429 on job submission, the job queue is full — retry the submission after a short delay.

Does the estimated_seconds field in the job submission response update during generation?
No. estimated_seconds is a static estimate based on model, resolution, and duration at submission time. Actual generation time can vary based on current model load.


The complete async pattern is: POST to submit, store the job ID, sleep the first-wait duration, then poll on an appropriate interval until completed. Handle failed status explicitly, persist job IDs so you can resume polling after a restart, and download the video URL before the 24-hour expiry. For Veo 3.1 model details and request parameters, start with the Veo 3.1 API guide.

Related posts

Join our DiscordAsync Video Generation API — How to Poll for Results (2026) — GenRelay