Problem RestatementProblem
Design a service like OpenAI's Sora. A user types a prompt ("a corgi surfing at sunset"), and the system generates a video. Generation takes minutes on expensive GPUs, and GPUs are scarce and sometimes fail. So the request can't be synchronous. It is an asynchronous job: the user submits, sees progress, can cancel, and downloads the video when it's ready.
Paid tiers get higher priority and larger quotas. The interviewer usually cares less about the ML model and more about jobs, scheduling and reliability around scarce compute.
RequirementsRequirements
1.1 Functional
- Submit a prompt with options (length, resolution) → get a
job_id. - See status and progress: queued (with position), running (percent), done or failed.
- Cancel a job.
- Download or stream the finished video, and see history.
- Quotas and priority per tier (free, plus, pro).
1.2 Non-Functional
- No lost jobs: a submitted job eventually completes or fails clearly.
- Efficient GPU use: GPUs should rarely sit idle, and work isn't done twice.
- Fairness: free users still make progress, and paid users wait less.
- Cost control: stop runaway usage and abuse.
1.3 Scale Estimates
- 1M videos/day ≈ 12 jobs/sec on average, with peaks of 50/sec.
- Each video needs ~2 GPU-minutes on average → 2M GPU-minutes/day ≈ 1,400 GPUs busy on average, more at peak. That's why queues form.
- Output: ~20 MB per video → 20 TB/day to object storage, served via CDN.
1.4 API Design
POST /v1/videos(Idempotency-Key){ prompt, duration_s: 10, resolution: "1080p" }→{ job_id, status: "queued" }GET /v1/videos/{job_id}→{ status, progress, queue_position, video_url? }POST /v1/videos/{job_id}/cancel- Optional webhook or SSE for progress updates.
High-Level ArchitectureArchitecture
2.1 Overview
- Job API: validates, checks quota and moderation, creates the job and returns immediately.
- Job DB: the durable job state (source of truth).
- GPU Scheduler: keeps priority queues per tier and assigns jobs to free GPU workers.
- GPU workers: run the model. They send heartbeats and progress, and save checkpoints.
- Post-processing workers (CPU): encode, make thumbnails, add a watermark.
- Object storage + CDN: final videos and intermediate checkpoints.
- Notification: tells the user when the job is done.
2.2 Architecture Diagram
%%{init: {"look":"handDrawn","handDrawnSeed":7,"theme":"base","fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","themeVariables":{"fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","fontSize":"16px","primaryColor":"#fff4e6","primaryBorderColor":"#1e1e1e","primaryTextColor":"#1e1e1e","secondaryColor":"#e7f5ff","tertiaryColor":"#ebfbee","lineColor":"#1e1e1e","textColor":"#1e1e1e","mainBkg":"#fff4e6","nodeBorder":"#1e1e1e","clusterBkg":"#f8f9fa","edgeLabelBackground":"#ffffff","classText":"#1e1e1e"}}}%%
flowchart LR
U["User"] --> API["Job API - quota, moderation"]
API --> DB[("Job DB")]
API --> SCH["GPU Scheduler - tier queues"]
SCH -->|"lease job"| G1["GPU worker"]
SCH -->|"lease job"| G2["GPU worker"]
G1 -->|"heartbeat, progress"| SCH
G1 --> CK[("Checkpoints - object storage")]
G1 --> PP["Post-process: encode, thumbnail"]
PP --> OS[("Videos - object storage")]
OS --> CDN["CDN"]
SCH --> DB
PP --> N["Notify user"]
U -->|"poll status / download"| APIData ModelData model
jobs:
job_id, user_id, tier, prompt, params, status (queued, running, postprocessing, succeeded,
failed, cancelled), priority, attempt, worker_id, lease_until, progress, checkpoint_key,
output_key, gpu_seconds_used, created_at, started_at, finished_at
usage:
user_id, period, gpu_seconds_used, videos_count -- for quotasKey FlowsFlows
4.1 Submit
- Check the prompt against content policy, and check the user's quota (e.g., 50 videos/month) and number of concurrent jobs.
- Create the job as
queued(the idempotency key stops double submits) and add it to the scheduler queue for the user's tier. - Return
job_id. The client polls every few seconds or listens on SSE.
4.2 Run
- When a GPU worker is free, the scheduler picks the next job (see fairness below) and leases it to the worker:
lease_until = now + 60s. - The worker heartbeats every ~15 seconds with progress, extending the lease.
- It saves checkpoints every few minutes (e.g., finished segments), so a crash doesn't lose all the work.
- When done, it uploads raw output, and post-processing encodes it. The job becomes
succeededand the user is notified.
4.3 Cancel
Mark cancelled. The worker sees it in the next heartbeat response, stops and frees the GPU.
Deep Dive A — Sharing GPUs that everyone wantsDeep dive
A job takes minutes on a GPU that costs dollars an hour, and there are never enough. How the queue is arranged is the product for everybody who is waiting.
One queue, first come first served
Every job goes into one FIFO. Fair in the plainest sense, and easy to reason about.
Paying customers wait behind free-tier jobs, which is the wrong business answer, and one user submitting fifty jobs occupies the fleet for an hour. FIFO also has no way to express that a 4-second 480p clip and a 60-second 1080p render are different work — they queue identically and the short job waits behind the long one.
A queue per tier, strict priority
Pro, plus and free each get a queue, and the scheduler always drains the highest tier first. Paying users now get their GPUs.
Strict priority starves the bottom. Whenever pro demand covers the fleet — which is most of the working day — free jobs never run at all, not slowly. The free tier stops being slow and becomes broken, and it is the tier that produces the paying users.
Weighted fair sharing, with a per-user ceiling
Give each tier a share of finishing slots instead of absolute precedence: of every 10 slots that free up, 6 to pro, 3 to plus, 1 to free.
%%{init: {"look":"handDrawn","handDrawnSeed":7,"theme":"base","fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","themeVariables":{"fontFamily":"Virgil, \"Segoe Print\", \"Comic Sans MS\", cursive","fontSize":"16px","primaryColor":"#fff4e6","primaryBorderColor":"#1e1e1e","primaryTextColor":"#1e1e1e","secondaryColor":"#e7f5ff","tertiaryColor":"#ebfbee","lineColor":"#1e1e1e","textColor":"#1e1e1e","mainBkg":"#fff4e6","nodeBorder":"#1e1e1e","clusterBkg":"#f8f9fa","edgeLabelBackground":"#ffffff","classText":"#1e1e1e"}}}%%
flowchart LR
Q1["Pro queue"] --> W["Weighted scheduler - 6 / 3 / 1 of free slots"]
Q2["Plus queue"] --> W
Q3["Free queue"] --> W
W --> CAP{"User already running 2 jobs?"}
CAP -->|"yes"| HOLD["Hold - next user's turn"]
CAP -->|"no"| POOL["Place by job size"]
POOL --> SMALL["Short / 480p pool"]
POOL --> BIG["Long / 1080p - bigger or multi-GPU"]Three things fall out of this that the earlier rungs could not express:
- Nobody starves. The free tier is slower, which is the intended product difference, and it always moves.
- A per-user cap of two running jobs stops one account from spending a tier's whole share.
- Right-sized placement. Long and high-resolution jobs go to a pool with bigger GPUs, so they stop blocking short ones.
Then show a queue position and ETA from queue depth and average job time. It costs almost nothing and removes the behaviour that hurts most: users cancelling and resubmitting because nothing appears to be happening, which throws away the GPU minutes already spent.
Deep Dive B — Failures without duplicate expensive workDeep dive
- Lease expiry: if heartbeats stop (the worker crashed), the scheduler re-queues the job with
attempt + 1, starting from the last checkpoint rather than from zero. - Fencing: a "zombie" worker from attempt 1 that wakes up late can't overwrite the result, because writes include the attempt number, and only the current attempt is accepted.
- Retry limits: after 3 failed attempts, mark
failed, refund the user's quota, and alert if many jobs fail (a bad model version or bad nodes). - Idempotent outputs: results are written to
videos/{job_id}/attempt-{n}.mp4, and the job record points to the winner. - Cost controls: hard caps on duration and resolution per tier, per-user daily GPU-second budgets, and alerts on unusual spending.
Trade-offs & AlternativesTrade-offs
| Decision | Choice | Why | Alternative |
|---|---|---|---|
| Request style | Async job + polling/SSE | Minutes-long work | Synchronous: timeouts, wasted GPU on disconnect |
| Scheduling | Weighted fair queues per tier | Paid priority without starving free | Strict priority: free tier may never run |
| Reliability | Leases + heartbeats + checkpoints | Recover without redoing everything | Restart from scratch: wastes GPU time |
| Status updates | Polling + optional SSE | Simple, robust | WebSocket only: more connection management |
Common Follow-up QuestionsFollow-ups
- "How do you plan capacity?" Track queue wait time per tier. When pro waits exceed the target, add GPUs (slow, since reserved capacity is bought ahead) or temporarily lower free-tier limits.
- "Multi-stage pipelines?" Model stages (draft → upscale → audio) as separate tasks in a small DAG, each with its own queue and checkpoint.
- "Abuse?" Moderate prompts before queueing and outputs before release, and rate-limit new accounts.
Wrap-UpWrap-up
Make video generation an asynchronous job: validate and store it, queue it by tier, and let a GPU scheduler lease jobs to workers with heartbeats and checkpoints. Use weighted fair queues and per-user limits to share scarce GPUs, fencing and attempt numbers to stop duplicate work, and quotas and cost caps to control spending. Store results in object storage behind a CDN and report progress through polling or SSE.