Integrating Seedance 2.0 Into a Production Render Queue
Async queue pattern for Seedance 2.0: submit with webhooks, poll for status, track cost per job, and retry around the face filter without burning budget.
The first time you run Seedance 2.0 is through fal.subscribe in a notebook. That works until someone wants 300 renders a week and your laptop is not a viable job runner. Here is the pattern for moving it onto a real queue with webhooks, per-job cost tracking, face filter retries, and hard timeouts.

What the queue has to handle
Submit without blocking the request thread. Handle webhook callbacks or poll for status. Retry recoverable failures, which for Seedance 2.0 is mostly face filter rejections. Track dollar cost per job, not credits.
Async submit with webhooks
A 720p Seedance 2.0 render at 15 seconds takes a minute or more. Blocking a request thread that long will take your API down under load. Use fal.queue.submit and let fal call you back.
01import { fal } from "@fal-ai/client";0203fal.config({ credentials: process.env.FAL_KEY });0405async function submitRender({ prompt, duration = 8, aspectRatio = "16:9" }) {06 const submitted = await fal.queue.submit(07 "bytedance/seedance-2.0/text-to-video",08 {09 input: {10 prompt,11 duration,12 resolution: "720p",13 aspect_ratio: aspectRatio14 },15 webhookUrl: "https://api.yourco.com/hooks/seedance"16 }17 );18 return submitted.request_id;19}
The webhook fires when the job reaches a terminal state. Handle success and failure on the same endpoint. Keep a database row per job keyed on request_id.
01app.post("/hooks/seedance", async (req, res) => {02 const { request_id, status } = req.body;03 if (status === "OK") {04 const result = await fal.queue.result(05 "bytedance/seedance-2.0/text-to-video",06 { requestId: request_id }07 );08 await saveOutput(request_id, result.data.video.url);09 } else {10 await markFailed(request_id, req.body.error);11 }12 res.status(200).end();13});
No webhook endpoint? Fall back to polling with fal.queue.status. Poll every five seconds, cap at 180 attempts for a fifteen minute ceiling.
Cost tracking per job
Seedance 2.0 bills at a dollar rate per second of output. Store that rate in a table. On submission, compute expected cost as rate x duration and save it with the job record. On completion, confirm duration and mark cost as final.
01async function recordJob({ requestId, prompt, duration, ratePerSecond }) {02 const expectedCost = ratePerSecond * duration;03 await db.jobs.insert({04 request_id: requestId,05 prompt,06 duration,07 expected_cost_usd: expectedCost,08 status: "submitted",09 submitted_at: new Date()10 });11}
Dollar amounts beat credit counts when a producer asks "what did this campaign cost." You answer with a SQL query. Credits require conversion every time.

Retry logic for the face filter
Seedance 2.0 has a face filter that can reject briefs describing identifiable people without enough abstraction. The rejection looks like a generic failure. The signal you want is the reason in status.logs.
The rule that works: on first failure, check the log for identity or likeness. If present, do not retry the same prompt. Strip identifying language (swap "a man who looks like a named actor" for "a man with a weathered face, mid fifties") and resubmit. If the failure is not identity related, retry once at the same parameters. If that fails, flag for review. Never loop retries on the same seed.
01async function handleFailure(requestId, reason) {02 if (/identity|likeness|face/i.test(reason)) {03 await markNeedsRewrite(requestId);04 return;05 }06 const job = await db.jobs.find(requestId);07 if (job.retry_count < 1) {08 const newId = await submitRender(job.input);09 await linkRetry(requestId, newId);10 } else {11 await markFailed(requestId, "exceeded retries");12 }13}
Hard timeouts at 15 seconds
Seedance 2.0 caps output at 15 seconds. Your queue should cap submission duration at the same place. A job sitting longer than eight minutes is stuck, not slow. Kill it, record the failure, move on.
What to monitor
First pass success rate, retry count per accepted shot, wall time from submit to webhook, dollar cost per shot. If first pass drops below 75 percent, audit prompts. If retry count climbs above 1.3, pre-flight is letting too many identity briefs through.