
Most teams can ship an AI video demo in a weekend. Keeping that demo alive when thousands of people press Generate at the same time is a different discipline. Video generation is close to a worst-case workload for a naive web stack: requests take minutes instead of milliseconds, jobs consume expensive accelerator time, outputs are measured in hundreds of megabytes, and a single failed render can waste an entire slice of compute budget.
This guide walks through how to build the backend spine for that product, using NestJS as the structural framework. It assumes you already have a generation endpoint that works and now need it to survive real traffic.
The failure modes arrive in a predictable order. First, HTTP workers get pinned waiting on long-running model calls, so the API stops answering health checks and the load balancer starts cycling pods. Next, the queue grows without bound and users lose the ability to tell whether their job is queued, running, or dead. Then storage costs spike because every retry writes a fresh copy of the same asset. Finally, an expensive model is invoked with parameters that were never validated, and the resulting artifacts are unusable.
Each of these has a structural fix, and each fix depends on where you draw boundaries in the codebase. The core problem is that a video pipeline is not request-response. It is a durable state machine with expensive transitions, and treating it like a REST endpoint is the root cause of most early scaling pain.
NestJS suits this shape well because its module system maps naturally onto pipeline stages. You isolate intake, routing, scheduling, persistence, and delivery, then let them communicate through narrow interfaces. Swapping a queue library or a storage vendor becomes a change in one module instead of a search-and-replace across the repo.
A video pipeline passes structured objects between many stages: an incoming job request, a resolved model descriptor, a normalized parameter set, a render manifest, a progress event. When those shapes drift, failures are silent. A worker reads durationSeconds while the API wrote duration_seconds, and the default value quietly truncates every output to five seconds.
Define those shapes once in a shared package and import them on both sides. Add runtime validation at the boundary using a schema library, so untrusted input is rejected before it reaches an expensive model. Static types cannot protect you from a malformed request body; validation schemas can, and the framework's pipe system makes that enforcement declarative rather than scattered.
A practical decomposition looks like this:
Each module exposes a narrow provider interface and hides its implementation. When you move from one queue backend to another, only the scheduler changes. Dependency injection handles wiring, and the testing utilities let you override providers so unit tests never touch a real queue or a real bucket.
NestJS gives you structure, not magic. It will not make a two-minute render fast, deduplicate assets for you, or decide which jobs deserve priority. Those remain design problems. Expect to write the scheduling policy, the storage lifecycle rules, and the recovery logic yourself. The framework is the skeleton; the muscle is yours.
Every generation request should produce exactly one durable job, even when the client retries. Derive the job identifier from a client-supplied idempotency key plus a server-side hash of the normalized request. The first write wins; identical follow-up requests return the existing job instead of enqueueing a duplicate render. This single pattern eliminates the most common source of runaway compute spend.
Intake should be fast. Validate the payload, check the account's remaining generation allowance, write a queued row, enqueue a lightweight message, and return a job identifier with a status URL. Do not hold the connection open while the model runs. Anything above a few hundred milliseconds of intake latency becomes a throughput ceiling long before the accelerators are busy.
One queue is a trap. Short preview renders, long high-resolution renders, upscaling passes, and audio generation have very different execution times, memory footprints, and failure characteristics. Put them in separate lanes with independent concurrency limits and worker pools.
A workable starting point is three lanes: an interactive lane for low-latency previews, a standard lane for full renders, and a batch lane for bulk or experimental work. Derive each lane's concurrency ceiling from measured resource consumption rather than guesswork. If a preview worker uses one-eighth of an accelerator and a full render uses a whole one, the limits should reflect that ratio.
Users tolerate waiting; they do not tolerate silence. Emit progress events at meaningful checkpoints such as queued, model loading, sampling, post-processing, encoding, and upload. Push them over a websocket with a polling fallback. Never invent a smooth percentage. Report stages honestly, and include an estimated remaining range only when historical data supports it.
Cancellation deserves first-class support. A cancelled job should stop consuming capacity at the earliest safe checkpoint, release its slot, and mark itself cancelled in the database. Store the cancellation flag in a low-latency store that workers poll between stages, and treat a missing flag as continue, so a caching outage never halts production.
Finally, decouple completion from delivery. When a render finishes, write the asset, update the job row, and publish a completion event. Webhook delivery, in-app notification, and email all subscribe independently, so a failing notification channel never blocks a successful render.
Routing logic that grows into a giant switch statement on model identifier becomes unmaintainable quickly. Build a model registry instead: a table or configuration file describing each model's capabilities (text-to-video, image-to-video, upscaling, lip sync), its input schema, typical render duration, hardware requirements, and a version identifier.
The routing module queries the registry, matches the incoming request against declared capabilities, and returns a fully resolved job specification. Adding a new model becomes a data change instead of a code change in three places, and capacity planning gets easier because you can query the registry for every model that requires a specific accelerator class.
Different models accept different parameter names and ranges. Normalize at the edge: translate a canonical request shape into each model's dialect inside an adapter, so upstream code never learns model-specific quirks. Clamp numeric ranges, strip unsupported fields, and reject contradictory combinations before they reach the worker.
Prompts deserve the same care. Trim whitespace, cap the length with a clear error rather than a silent truncation, and store the exact prompt text alongside the job so results are reproducible. If you support negative prompts or seeds, persist them even when a particular model ignores them. Future re-renders will benefit.
When a model is unavailable, degraded, or returning malformed output, an ordered fallback chain keeps the product usable. Define alternatives per capability and record which one actually served the job, so support conversations are factual rather than speculative. Above all, never silently swap a high-fidelity model for a lower-quality one. Users notice, and trust is harder to rebuild than a queue backlog.
Keep the relational side lean: job identifier, account reference, status, model version, normalized parameters, timestamps, asset keys, and error codes. Store pixels in object storage. Base64 media in the database will wreck backup windows and replication lag long before it becomes a performance problem you can tune away.
Index the queries you actually run: recent jobs per account, jobs by status for operational dashboards, and jobs by model version for quality analysis. Partition the job table by month once it passes tens of millions of rows.
Never expose raw storage paths. Generate short-lived signed links at delivery time and tie them to the requesting identity. Set lifecycle rules so abandoned intermediates expire automatically. Temporary frames, audio stems, and failed render artifacts should not accumulate indefinitely; they are pure cost with no user value.
Deduplicate by content hash where it is safe. Identical parameter sets that produce deterministic output can share an asset. Anything with randomized sampling should not. Mark which models are deterministic in your registry so the deduplication layer can make an informed decision instead of guessing.
Cache aggressively at the edges of the pipeline and rarely in the middle. Registry lookups, capability matrices, and account allowance snapshots are excellent candidates with short time-to-live values. Render output itself is rarely cacheable unless requests are truly identical, and a stale hit that returns the wrong video is worse than a re-render. Prefer immutable, content-addressed keys with long lifetimes over mutable keys with short ones.
Priority queues are easy to add and easy to ruin. Give every job a class such as interactive, standard, or batch, then allocate capacity as a share of the pool rather than a strict ordering. A strict priority queue means a burst of interactive jobs starves batch work indefinitely. Weighted fair scheduling, with a reserved share per class, keeps everything moving.
Cold starts dominate latency for short renders. Keep a small warm pool of workers per accelerator class, sized to expected burst traffic. Model loading can take longer than the render itself, so a warm worker holding weights in memory is worth far more than an extra queue slot.
Batch sizing is a balancing act. Larger batches improve throughput per accelerator-hour but increase tail latency and memory pressure. Measure both. If the median render is forty seconds and batching pushes the ninety-fifth percentile past two minutes, you have optimized the wrong metric.
Enforce per-account concurrency limits at intake, not inside the worker. A single account submitting five hundred jobs should not occupy every slot. Track in-flight counts in a fast store, reject over-limit submissions with a clear retry-after header, and make the limits tunable per plan tier without a deploy. Distinguish a legitimate power user from an automated script by examining submission patterns, not just raw volume.
A single generation touches an API, a queue, at least one worker, and a storage layer. Without distributed tracing, debugging a slow job is guesswork. Propagate a trace context from the HTTP request through the queue message into the worker, and attach spans for each pipeline stage. When someone asks why a job took nine minutes, the answer should be one trace view, not an afternoon of log archaeology.
Retries are mandatory; blind retries are dangerous. Classify errors as transient (network issues, temporary capacity), permanent (invalid parameters, policy violations), or unknown. Retry transient failures with exponential backoff and jitter, cap the attempts, then move the message to a dead-letter queue for inspection.
Every worker must be idempotent. A retried job that re-encodes and uploads a second copy doubles storage usage and confuses downstream consumers. Write outputs to a deterministic key derived from the job identifier, and treat already-exists as a success condition rather than an error.
Track queue depth by lane, time-in-queue distributions, worker utilization, failure rate by model version, and storage growth rate. Queue depth alerts are the single most useful early warning in this kind of system: by the time users complain, the backlog is already hours deep. Pair every alert with a runbook step so the on-call engineer does not have to invent a response at three in the morning.
API pods and workers have different scaling triggers. Scale the API on request rate and CPU, and scale workers on queue depth and accelerator utilization. Coupling them guarantees you will either pay for idle workers or throttle intake while capacity sits unused. Keep them in separate deployments with separate autoscaling policies.
Model endpoints, provider keys, storage buckets, and concurrency limits should all be environment configuration, never hardcoded. Validate configuration at startup and fail fast, because a service that boots with a missing bucket name will fail confusingly forty jobs later. Keep secrets in a managed store, inject them at runtime, and rotate them without a redeploy.
A new model is a production change. Roll it out behind a flag to a small share of traffic, compare output quality and latency against the incumbent, and only then shift the default. Keep the previous version reachable so you can roll back instantly. Record the model version on every job so a quality regression can be traced to a specific rollout window.
Framework overhead is negligible compared to model inference. The Node.js runtime handles thousands of concurrent queue operations comfortably. Your bottleneck will be accelerators, storage throughput, and network egress, not the HTTP layer.
No. Keep the API process thin and run workers as separate processes or containers. They have different scaling triggers, different failure characteristics, and different resource profiles. Mixing them means a stuck render can take down your API.
Use a Redis-backed queue library when you want simplicity and per-job visibility at throughput measured in thousands per hour. Choose RabbitMQ when complex routing topologies matter. Choose Kafka when you need a durable log with replay across many independent consumers. Most video platforms start with the first and only move when a specific limitation appears.
No. Managed container platforms handle early scale well. Move to Kubernetes when you need GPU node pools with different accelerator classes and fine-grained scheduling across them.
Break the pipeline into checkpointed stages and use heartbeats to extend visibility timeouts. Design each stage so a restarted worker resumes from the last completed step instead of starting over. Store intermediate artifacts in object storage between stages so state survives a crash.
Time in queue, segmented by lane. It is the earliest signal that intake is outpacing capacity, and it moves before error rates or latency percentiles do. Alert on it, and you will usually have time to react before users notice.

Alexander
Vidu Q4 alternatives compared: Kling 4.0, Seedance 2.5, Wan 3.0, and Runway Gen-4 โ 4K support, input modes, durations, audio, and what each costs in credits on Domer.
The top 4K AI image generators on Domer compared: Flux 3 Klein, Nano Banana 2.1, GPT Image 2.5, GPT Image 2, and Nano Banana Pro โ 4K credit costs, reference limits, and speed.
Nano Banana 2.1 vs GPT Image 2.5 Flare compared: credit costs per resolution, aspect ratios, reference images, text rendering, and which tasks fit each model.