Feature 01Integration

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.

By seedance2-api editorial..5 min read

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.

Queue architecture overview
Queue architecture overview

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.

01example.tsJAVASCRIPT
01import { fal } from "@fal-ai/client";
02
03fal.config({ credentials: process.env.FAL_KEY });
04
05async 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: aspectRatio
14 },
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.

01example.tsJAVASCRIPT
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.

01example.tsJAVASCRIPT
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.

Per-job cost ledger
Per-job cost ledger

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.

01example.tsJAVASCRIPT
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.


00Back to the archive
Also reading