Moving shot generation onto a queue
The new BullMQ worker, the two feature flags that route to it, and why image-before-video is a hard precondition — plus what’s still synchronous by default.
For most of VidFlow's life, shot generation ran inline: a request came in, the server generated the shot, and the request waited. That's simple and easy to reason about — and it doesn't scale past one project at a time. This is the story of moving that work onto a queue, and an honest account of how far the migration actually is.
The old path
Per-shot generation happened synchronously inside the orchestrator. One project's shots ran in series, bounded by the request lifecycle. It works, it's still the default, and for a single project it's perfectly fine. The ceiling is concurrency: you can't easily have several projects' shots generating at once without tying up workers.
The new path
There's now a separate worker process — npm run worker — that pulls generation work off Redis-backed BullMQ queues. Per-shot generation enqueues to a generate-shot queue, and the orchestrator awaits the result via an enqueueAndAwait helper. The web process stays responsive; the heavy lifting moves to the worker, which can chew through multiple projects' shots in parallel.
Two flags, on purpose
The migration is gated so we can roll it out carefully. JOBS_GENERATE_SHOT_NEW=1 routes shot regeneration through the worker. JOBS_ORCHESTRATOR_NEW=1 routes the orchestrator's production stage through it too — per-shot generation enqueues and awaits. You set both to put the whole production path on the queue; set neither and you're on the legacy synchronous path. Flags, not a hard cutover, because the failure modes of a distributed queue are different from an inline call and we wanted to prove them out before flipping everyone.
Image before video, always
Phase 1.5 wired the real video model, and it enforces a hard precondition: a shot's image must exist before its video generates (shot.imageUrl is the gate). Video models take the still as their first frame — generating video without the image isn't a degraded result, it's a wrong one. The worker refuses to start the video step until the image step has landed.
Where the work comes back
Generation is async and vendor-driven, so results don't return inline — vendor webhooks land at /api/{vendor}/callback, and the worker reconciles them against the queued jobs. That's the part the synchronous path never had to think about, and the part most worth testing.
The honest status
Production video generation still runs synchronously by default. The queue path is built and exercised, but it is not the prod default yet — a canary rollout is the gating step, not a flag flip in the dark. So this post describes a road that's paved and driven, not yet the main highway.
Trying it locally
Spin a fresh project with mock vendors and both flags on: MOCK_VENDORS=1 JOBS_ORCHESTRATOR_NEW=1 JOBS_GENERATE_SHOT_NEW=1 npm run worker (with matching env on npm run dev), trigger a run from the UI, and watch shots complete through the worker logs. That's the same path we're hardening toward making default.

