A newer version of the Gradio SDK is available: 6.20.0
Research: Building an Orchestrator/Queue System for Hugging Face ZeroGPU Beyond 120s
Date: 2026-06-06 Status: Research brief with honest feasibility assessment
Executive Summary
ZeroGPU is not viable for tasks that genuinely exceed ~120 seconds of continuous GPU work. The hard limit is enforced at the infrastructure level β the forked GPU process is killed by the ZeroGPU scheduler, and this kill cannot be caught or intercepted. However, there is a meaningful middle ground: tasks that conceptually need more than 120s can be decomposed into discrete GPU calls of β€120s each, with state persisted between calls. This requires careful architecture and comes with significant trade-offs. For production workloads that need unbounded GPU time, dedicated GPU hosting ($0.50β$5.00/hr) is the honest answer.
1. ZeroGPU Limitations: What Actually Happens
1.1 The 120-Second Timeout
Hard limit: 120 seconds. The @spaces.GPU(duration=...) parameter accepts values up to 120 seconds reliably. Values between 120β240s sometimes partially work but are unreliable and may be rejected with "ZeroGPU illegal duration" errors. Values above 240s are rejected outright. (HF Forums β illegal duration, zero-gpu-explorers discussions)
What happens at timeout: The ZeroGPU runtime forks the main process, attaches a GPU slice, runs the decorated function, and then kills the fork. At timeout, the fork is killed externally β this is not a Python exception you can catch with try/except. The process receives a kill signal (effectively SIGKILL), and the GPU slice is torn down. The Gradio UI sees a "GPU Task aborted" or "ZeroGPU worker error" message. (HF blog β zerogpu-aoti, HF Forums β GPU Task aborted)
Key implications:
- No
try/finallycleanup runs at timeout β the process is just dead - No graceful shutdown, no state serialization at timeout boundary
- The main process (Gradio server) survives; only the fork dies
- You cannot heartbeat or keepalive your way past the timeout
1.2 State Persistence Between Calls
Model loading works across calls. Models loaded at module level with .to('cuda') use ZeroGPU's CUDA emulation mode. The model weights reside in the main process's memory (CPU-side CUDA emulation). When @spaces.GPU is called, the fork inherits the process memory space including model weights, and a real GPU is attached. The model is effectively "warm" for each fork. (Official docs)
What persists:
- Model weights loaded at module level (inherited by each fork)
- Global Python state (module-level variables, caches)
- Files on disk (ephemeral β lost on Space restart)
- Files in HF Storage Buckets (persistent across restarts)
What does NOT persist:
- GPU memory state (each fork starts with a fresh GPU)
- CUDA context, kernels, compiled graphs (unless AoT-compiled and saved to disk)
- In-progress tensor computations
1.3 Re-entrant / Multiple @spaces.GPU Calls
Yes, you can call @spaces.GPU functions multiple times from within the same request. Each call triggers a separate fork β GPU allocation β execute β kill cycle. The main process orchestrates these calls sequentially.
# This IS valid - two separate GPU calls in one request
@spaces.GPU(duration=60)
def encode_text(prompt):
return text\_encoder(prompt)
@spaces.GPU(duration=60)
def run_diffusion(latents, steps):
return unet(latents, num\_inference\_steps=steps)
def generate(prompt):
latents \= encode\_text(prompt) \# First GPU call
images \= run\_diffusion(latents, 20\) \# Second GPU call
return images
Caveat: Each call consumes the user's GPU quota independently. Two 60s calls = 120s of quota consumed. The user waits through two separate GPU queue + allocation cycles.
1.4 Duration Quota System
| Account Type | Daily GPU Quota | Queue Priority |
|---|---|---|
| Unauthenticated | 2 minutes | Low |
| Free account | 5 minutes | Medium |
| Pro ($9/mo) | 40 minutes | Highest |
| Team/Enterprise | 40β60 minutes | Highest |
Pro/Team/Enterprise users can extend beyond quota using credits at $1 per 10 minutes of GPU time. (Official docs)
Important: Quota is charged based on the reserved duration, not actual runtime. A @spaces.GPU(duration=120) call that finishes in 30s still consumes 120s of quota.
1.5 Hardware Specs
Current ZeroGPU hardware: NVIDIA RTX Pro 6000 Blackwell (successor to H200).
large(default): half GPU, 48GB VRAM, 1Γ quota costxlarge: full GPU, 96GB VRAM, 2Γ quota cost
2. Existing Solutions: How Big Demos Handle This
2.1 The AoT Compilation Strategy (Most Successful)
The official Hugging Face approach for video generation (Wan, LTX, Flux) is not to work around the timeout β it's to make inference fast enough to fit within 120s. The blog post Make your ZeroGPU Spaces go brrr with ahead-of-time compilation documents 1.3Γβ1.8Γ speedups using:
- PyTorch AoT compilation (
torch.export+AOTInductor) β compile once, reload instantly - FP8 quantization β additional 1.2Γ speedup on H200's FP8 hardware
- Flash Attention 3 β pre-built kernels via
kernelslibrary - Regional compilation β compile repeated transformer blocks once, reduce cold-start from 6min to 30sec
Real-world example: The LTX-Video Playground uses @spaces.GPU(duration=120) with dynamic duration estimation:
def get_duration(steps, duration_seconds, ...):
if steps \> 4 and duration\_seconds \> 2:
return 90
elif steps \> 4 or duration\_seconds \> 2:
return 75
else:
return 60
@spaces.GPU(duration=get_duration)
def generate_video(...):
...
Real-world example: The Wan 2.2 AoTI Space uses a sophisticated duration estimator:
def get_inference_duration(...):
BASE\_FRAMES\_HEIGHT\_WIDTH \= 81 \* 832 \* 624
BASE\_STEP\_DURATION \= 15
factor \= num\_frames \* width \* height / BASE\_FRAMES\_HEIGHT\_WIDTH
step\_duration \= BASE\_STEP\_DURATION \* factor \*\* 1.5
gen\_time \= int(steps) \* step\_duration
return max(60, min(gen\_time, 120))
2.2 The "Keep It Short" Pattern
Most production ZeroGPU Spaces simply limit what users can request:
- Cap resolution (512Γ512 instead of 1024+)
- Cap step count (20β30 steps)
- Cap video length (2β4 seconds)
- Offer quality presets (Fast/Balanced/Quality) with different step counts
This is the dominant pattern. No major demo tries to exceed 120s.
2.3 cbensimon's ZeroGPU Queue
The Space cbensimon/zero-gpu-queue is a minimal test of ZeroGPU queue mechanics. The related Gradio PRs (#4937, #5129) fix Gradio's internal queue max_size and concurrency_count for ZeroGPU. This is about queue management, not about exceeding the timeout.
2.4 Agent Zero Orchestration
ScottzillaSystems/agent-zero-orchestration uses @spaces.GPU(duration=180) for agent loops. This pushes past 120s but relies on the "sometimes works up to 240s" zone β not reliable for production.
3. Architecture Patterns
Pattern A: Checkpoint-and-Resume (Feasible, Complex)
Concept: Break work into chunks, save intermediate state to disk/Storage Bucket between @spaces.GPU calls.
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Main Process (CPU) β
β ββββββββββββ ββββββββββββ ββββββββββββ β
β β Chunk 1 βββ>β Chunk 2 βββ>β Chunk N βββ> Result β
β β @GPU(60) β β @GPU(60) β β @GPU(60) β β
β ββββββ¬ββββββ ββββββ¬ββββββ ββββββββββββ β
β β β β
β v v β
β ββββββββββββββββββββββββββββββββ β
β β HF Storage Bucket /data β β
β β checkpoint_step_001.pt β β
β β checkpoint_step_002.pt β β
β ββββββββββββββββββββββββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Pseudo-code:
import spaces
import torch
import json
from pathlib import Path
from huggingface_hub import CommitOperationAdd, HfApi
STATE_DIR = Path("/data/checkpoints") # Storage Bucket mount
@spaces.GPU(duration=60)
def run_chunk(prompt_embeds, chunk_idx, total_chunks):
\# Load checkpoint if resuming
state\_path \= STATE\_DIR / f"chunk\_{chunk\_idx}.pt"
if state\_path.exists():
latents \= torch.load(state\_path)
else:
latents \= torch.randn(1, 4, 64, 64, device="cuda")
\# Run N diffusion steps for this chunk
steps\_per\_chunk \= 10
for step in range(steps\_per\_chunk):
latents \= unet\_step(latents, prompt\_embeds, step \+ chunk\_idx \* steps\_per\_chunk)
\# Save checkpoint
torch.save(latents.cpu(), state\_path)
return chunk\_idx \+ 1 \# Return next chunk index
def generate_long(prompt, total_steps=100):
prompt\_embeds \= encode\_prompt(prompt) \# CPU or quick GPU call
chunk\_size \= 10 \# steps per GPU call
total\_chunks \= total\_steps // chunk\_size
chunk\_idx \= 0
while chunk\_idx \< total\_chunks:
chunk\_idx \= run\_chunk(prompt\_embeds, chunk\_idx, total\_chunks)
\# Final decode
return decode\_latents(chunk\_idx)
Trade-offs:
- β Works within ZeroGPU constraints
- β Each chunk respects the timeout
- β Can resume from checkpoint if Space restarts
- β Latent tensor serialization to disk is slow (seconds per save)
- β Each GPU call goes through queue β allocate β fork β execute β kill cycle
- β User quota consumed per chunk (10 chunks Γ 60s = 600s quota for 100-step generation)
- β Gradio UI must stay alive across multiple sequential GPU calls
When it works: Video generation where you can chunk frames (e.g., generate 4 frames per GPU call, accumulate).
Pattern B: Step-Wise Execution via Generators (Feasible, Better UX)
Concept: Use Gradio's generator/streaming pattern to yield intermediate results while making sequential @spaces.GPU calls.
import gradio as gr
import spaces
import torch
pipe = ... # loaded at module level
@spaces.GPU(duration=30)
def diffusion_step(latents, prompt_embeds, step):
"""Run ONE diffusion step per GPU call."""
latents \= pipe.scheduler.scale\_model\_input(latents, step)
noise\_pred \= pipe.unet(latents, step, encoder\_hidden\_states=prompt\_embeds).sample
latents \= pipe.scheduler.step(noise\_pred, step, latents).prev\_sample
return latents
def generate_streaming(prompt, total_steps=50):
"""Generator that yields intermediate images every N steps."""
prompt\_embeds \= encode(prompt) \# quick
latents \= torch.randn(1, 4, 64, 64\)
for step in range(total\_steps):
latents \= diffusion\_step(latents, prompt\_embeds, step)
if step % 5 \== 0:
\# Decode and yield intermediate result
image \= decode(latents)
yield image, f"Step {step}/{total\_steps}"
yield decode(latents), "Done\!"
gr.Interface(
fn=generate\_streaming,
inputs=\[gr.Text(), gr.Slider(10, 100)\],
outputs=\[gr.Image(), gr.Text()\],
).launch()
Trade-offs:
- β Streaming UI shows progress
- β Each step is a separate GPU call with its own timeout
- β Extremely quota-expensive: 50 steps Γ 30s duration = 1500s quota for what should be a 20s job
- β Each step goes through GPU allocation queue (latency per step: 2β10s)
- β Model weights are loaded into GPU memory fresh for each fork (slow if not using AoT)
Honest assessment: This pattern is technically possible but wasteful. You're paying 25Γ in quota and 10Γ in wall-clock time for the privilege of streaming individual diffusion steps. The only scenario where it makes sense is if each "step" is itself a substantial computation (e.g., one frame of video generation that takes 30β60s).
Pattern C: Dedicated Queue Space + Worker Spaces (Advanced, Fragile)
Concept: One Space acts as a queue manager (CPU-only), delegating work to multiple ZeroGPU worker Spaces via API calls.
ββββββββββββββββββββββββββββββββ
β Queue Space (CPU-only) β
β - SQLite / in-memory queue β
β - REST API for job submit β
β - Polling for results β
β - Gradio UI for users β
ββββββββββββ¬ββββββββββββββββββββ
β HTTP API calls
v
ββββββββββββββββββββββββββββββββ
β Worker Space A (ZeroGPU) β
β @spaces.GPU(duration=120) β
β POST /run β process chunk β
ββββββββββββββββββββββββββββββββ
...
ββββββββββββββββββββββββββββββββ
β Worker Space B (ZeroGPU) β
β @spaces.GPU(duration=120) β
β POST /run β process chunk β
ββββββββββββββββββββββββββββββββ
How it works:
- User submits job via Queue Space's Gradio UI
- Queue Space writes job to HF Dataset or Storage Bucket
- Queue Space calls Worker Space's API endpoint (Gradio's REST API)
- Worker reads input from Storage, runs GPU computation, writes result to Storage
- Queue Space polls for result, assembles final output
Pseudo-code (Queue Space):
import gradio as gr
import requests
import time
from huggingface_hub import HfApi
WORKER_URL = "https://your-worker-space.hf.space/api/predict"
JOB_STORAGE = "username/job-storage" # HF Dataset
def submit_job(prompt, config):
\# 1\. Write job to storage
job\_id \= write\_job\_to\_dataset(JOB\_STORAGE, prompt, config)
\# 2\. Call worker
response \= requests.post(WORKER\_URL, json={
"data": \[job\_id, prompt, config\]
})
\# 3\. Poll for completion
while not job\_complete(JOB\_STORAGE, job\_id):
time.sleep(2)
yield None, "Processing..."
\# 4\. Assemble result
result \= read\_result\_from\_dataset(JOB\_STORAGE, job\_id)
yield result, "Done\!"
gr.Interface(submit_job, [gr.Text(), gr.JSON()], [gr.Image(), gr.Text()]).launch()
Trade-offs:
- β Can parallelize across multiple workers
- β Queue management is clean
- β 10 ZeroGPU Space limit per Pro account β can't scale to many workers
- β Each Space requires separate deployment and maintenance
- β Cross-Space communication adds latency (HTTP API + Storage I/O)
- β Each Space consumes the user's quota when they call it
- β Gradio's API client adds overhead; no native inter-Space RPC
Pattern D: Hybrid CPU Orchestration + GPU Bursts (Most Practical)
Concept: Do everything except inference on CPU in the main process, using @spaces.GPU only for the actual computation kernel. Use HF Scheduled Jobs for orchestration if needed.
import gradio as gr
import spaces
import torch
# Model loaded at module level (CUDA emulation handles .to('cuda'))
pipe = DiffusionPipeline.from_pretrained(...).to('cuda')
@spaces.GPU(duration=get_duration)
def gpu_kernel(prompt, steps, resolution, seed):
"""Single GPU call β all computation happens here."""
generator \= torch.Generator("cuda").manual\_seed(seed)
return pipe(
prompt,
num\_inference\_steps=steps,
height=resolution,
width=resolution,
generator=generator,
).images\[0\]
def get_duration(prompt, steps, resolution, seed):
"""Estimate duration to request minimal quota."""
if resolution \<= 512 and steps \<= 20:
return 30
elif resolution \<= 768 and steps \<= 30:
return 60
else:
return 120
# Limit what users can request to fit within 120s
with gr.Blocks() as demo:
prompt \= gr.Text()
resolution \= gr.Dropdown(\[256, 512, 768\], value=512)
steps \= gr.Slider(1, 50, value=20)
output \= gr.Image()
generate\_btn \= gr.Button("Generate")
generate\_btn.click(gpu\_kernel, \[prompt, steps, resolution, gr.Number(42)\], output)
demo.launch()
This is what the big demos actually do. They don't exceed 120s β they constrain the problem to fit.
Pattern E: HF Scheduled Jobs for Long Orchestration (Emerging)
HF now supports Scheduled Jobs that can run on GPU hardware:
hf jobs scheduled uv run --flavor a10g-small --with torch @hourly python orchestrator.py
This is a relatively new feature and could be used for:
- Periodic batch processing
- Pre-computing common outputs
- Fine-tuning jobs that run in scheduled windows
However, this uses paid dedicated GPU, not ZeroGPU.
4. Can You Pass Model State Between ZeroGPU Invocations?
Yes, but with caveats:
| Method | Works? | Speed | Notes |
|---|---|---|---|
Global model .to('cuda') at module level |
β Yes | Fast | Weights in main process memory, inherited by fork |
| Pickle/torch.save to ephemeral disk | β Yes | Slow | Lost on Space restart |
| Save to HF Storage Bucket | β Yes | Very slow | Network I/O per save |
| Save to HF Dataset | β Yes | Very slow | Git-based, not ideal for frequent writes |
| Keep tensor in global variable | β οΈ Partial | Fast | Survives between calls but NOT across Space restarts |
| CUDA graph persistence | β No | N/A | Each fork gets fresh GPU context |
Recommendation: For model weights, use the standard pattern (load at module level, .to('cuda')). For intermediate computation state, use ephemeral disk /data/ for speed, with HF Storage Buckets only if you need persistence across restarts.
5. The Gradio UI Timeout Problem
When chaining multiple @spaces.GPU calls, the Gradio UI must stay responsive. The key mechanisms:
- Generators (
yield): Gradio supports generator functions that yield intermediate results. The UI stays alive between yields. gr.Progress(): Shows a progress bar during long operations.- Queue mode: Gradio's queue system handles concurrent users.
Problem: If the total time across all GPU calls exceeds ~5 minutes, users may see "connection timed out" from Gradio's WebSocket. (HF Forums β connection timed out)
Solution: Use generator pattern with periodic yields to keep the WebSocket alive, and poll for completion rather than blocking.
6. Component Diagram: The Recommended Architecture
For tasks that genuinely need >120s and you insist on using ZeroGPU:
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β USER'S BROWSER β
β βββββββββββββββ ββββββββββββββββββββ ββββββββββββββββββββ β
β β Submit Job βββ>β See Progress βββ>β Download Result β β
β βββββββββββββββ β (polling/SSE) β ββββββββββββββββββββ β
β ββββββββββ¬ββββββββββ β
ββββββββββββββββββββββββββββββββΌββββββββββββββββββββββββββββββββββββββββββ
β WebSocket
ββββββββββββββββββββββββββββββββΌββββββββββββββββββββββββββββββββββββββββββ
β GRADIO SPACE (Main Process, CPU) β
β ββββββββββ΄ββββββββββ β
β β Job Orchestrator β β
β β (Python generator)β β
β ββββββββββ¬βββββββββββ β
β β β
β βββββββββββββββββββΌββββββββββββββββββ β
β v v v β
β ββββββββββββββββ ββββββββββββββββ ββββββββββββββββ β
β β @GPU(60) β β @GPU(60) β β @GPU(60) β β Sequential β
β β Chunk 1 β β Chunk 2 β β Chunk N β GPU forks β
β β encode+gen β β generate β β generate+dec β β
β ββββββββ¬ββββββββ ββββββββ¬ββββββββ ββββββββ¬ββββββββ β
β β β β β
β v v v β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
β β HF Storage Bucket (/data/) β β
β β job_123_chunk_1.pt β job_123_chunk_2.pt β result.pt β β
β ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Key design decisions:
- Single Space (not multi-Space orchestration) to avoid the 10-Space limit
- Generator pattern keeps UI alive between chunks
- State persisted to Storage Bucket between GPU calls
- Each chunk must complete in <60s (leaving buffer before 120s hard limit)
- Dynamic duration estimator minimizes quota waste
7. Honest Feasibility Assessment
Tasks where ZeroGPU + orchestration COULD work:
| Task | Feasible? | Strategy |
|---|---|---|
| Video gen (4β8s clips, optimized) | β Yes | AoT + FP8, fit in 120s |
| Batch image gen (10 images) | β Yes | Sequential @spaces.GPU calls, 1 image each |
| Long text generation (LLM streaming) | β Yes | Already works β streaming is natural |
| Image gen at high res (1024+) | β οΈ Maybe | AoT compilation + FP8 might squeeze it in |
| Video gen (>10s) | β οΈ Difficult | Chunk frames, checkpoint between calls |
| Fine-tuning / training | β No | Not what ZeroGPU is designed for |
| Batch processing (100+ items) | β No | Quota exhaustion after a handful of items |
| Real-time video gen (>30s) | β No | Fundamentally incompatible |
Tasks where ZeroGPU WILL NOT work:
- Model fine-tuning β requires minutes to hours of continuous GPU
- Large-scale batch processing β quota burns too fast
- Training / LoRA adaptation β optimizer state is hard to checkpoint between forks
- Real-time video longer than ~10s β even AoT can't make it fast enough
- Multi-stage pipelines with tight dependencies β inter-GPU-call latency kills throughput
8. Cost Comparison: ZeroGPU vs. Alternatives
ZeroGPU (if you could make it work)
| Tier | Cost | What You Get |
|---|---|---|
| Pro ($9/mo) | $9/month + $0.10/min over quota | 40 min/day GPU, H200-class, shared |
| Credits | $1 per 10 min GPU time | Extends beyond daily quota |
| Effective hourly rate | ~$6/hr (at credit rate) | But shared, queued, and time-limited |
Dedicated GPU Hosting (No Timeout)
| Provider | GPU | Price/hr | Notes |
|---|---|---|---|
| HF Inference Endpoints | T4 16GB | $0.50/hr | Cheapest GPU, good for inference |
| HF Inference Endpoints | A10G 24GB | $1.00/hr | Good mid-range |
| HF Inference Endpoints | A100 80GB | $2.50β$5.00/hr | Serious workloads |
| RunPod Secure Cloud | A100 80GB | $2.17/hr | Cheapest A100 |
| RunPod Secure Cloud | H100 80GB | $3.35/hr | Cheapest H100 |
| Modal | A100 80GB | $2.10/hr | Python-native, $30/mo free credit |
| Fal.ai | A100 80GB | $0.99/hr | Cheapest per-GPU-second |
| Replicate | A100 80GB | $5.04/hr | Simplest API, most expensive |
Sources: HF Endpoints pricing, RunPod pricing, HostFleet comparison, Replicate pricing
Cost Analysis Example: Processing 100 video generations at 5 min each
| Option | Total GPU Time | Cost |
|---|---|---|
| ZeroGPU (Pro credits) | 500 min | $50 in credits + $9/mo subscription |
| HF Endpoints (A10G) | 500 min = ~8.3 hrs | ~$8.30 (shut down between jobs) |
| RunPod Serverless (A100) | 500 min = ~8.3 hrs | ~$18.00 |
| Fal.ai (A100) | 500 min = ~8.3 hrs | ~$8.25 |
| Modal (A100) | 500 min = ~8.3 hrs | ~$17.40 ($30 free credit covers it) |
Conclusion: For any sustained GPU workload, dedicated hosting is cheaper than ZeroGPU credits. ZeroGPU's value is the free tier (40 min/day for Pro) for demos and prototyping, not production workloads.
9. Minimal-Cost Alternative Recommendations
Tier 1: "I need free/cheap and can tolerate constraints"
β ZeroGPU with AoT compilation. Optimize your model to fit in 120s. Use the strategies from the official blog post:
- AoT compile the transformer/denoiser
- Apply FP8 quantization
- Use Flash Attention 3
- Limit resolution, steps, video length in the UI
- Use dynamic duration to minimize quota waste
Tier 2: "I need reliable long-running GPU but want to stay cheap"
β Fal.ai or RunPod Serverless.
- Fal.ai A100 at $0.99/hr is the cheapest per-GPU-hour option available
- RunPod Serverless with per-millisecond billing for bursty workloads
- Both support custom Docker containers with your full pipeline
- No timeout constraints, no queue system to fight with
- Add a simple FastAPI + Redis queue on top for orchestration
Tier 3: "I need production-grade inference at scale"
β HF Inference Endpoints or Modal.
- HF Endpoints: managed, auto-scaling, integrates with HF Hub
- Modal: Python-native, excellent DX, $30/mo free credit, proper autoscaling
- Both support scale-to-zero to minimize idle costs
- Add Celery/Redis or Modal's native queuing for task management
Architecture for Tier 2/3:
βββββββββββββββ ββββββββββββ ββββββββββββββββ
β Gradio UI βββββ>β FastAPI βββββ>β Redis Queue β
β (any host) β<βββββ + Celery β<βββββ β
βββββββββββββββ βββββββ¬ββββββ ββββββββ¬ββββββββ
β β
v v
ββββββββββββββββββββββββββββββββββββ
β GPU Worker (RunPod/Modal/Fal) β
β \- Pulls from Redis queue β
β \- Runs full pipeline, no timeout β
β \- Writes result to S3/HF Storage β
β \- Marks job complete in Redis β
ββββββββββββββββββββββββββββββββββββ
10. Key Takeaways
The 120s timeout is a hard, uncircumventable limit. The forked process is killed externally β no Python exception, no graceful shutdown.
The big demos (Flux, Wan, LTX) do NOT exceed 120s. They use AoT compilation + quantization to make inference fast enough, and constrain user inputs to fit within the budget.
Chunking/checkpointing is theoretically possible but impractical for most use cases. Each chunk goes through the full GPU allocation pipeline (queue β allocate β fork β execute β kill), adding 5β15s overhead per chunk and burning quota at the reserved duration rate.
ZeroGPU is designed for demos, not production workloads. The daily quota, per-call timeout, shared infrastructure, and queue-based scheduling all point to this being a demo/prototyping tool.
If your task genuinely needs >120s of continuous GPU, use dedicated GPU hosting. At $0.50β$5.00/hr, it's cheaper than ZeroGPU credits and has no timeout constraints.
The most promising pattern for "long" tasks on ZeroGPU is: AoT compile + FP8 quantize to fit more work per second, use dynamic duration to minimize quota waste, and limit user-facing parameters to stay within 120s. For video: generate short clips (2β4s) and stitch client-side.
HF Storage Buckets are the right persistence mechanism for checkpointing between GPU calls β mounted as local filesystem, persists across Space restarts, S3-like semantics.
Sources
Official Documentation
- Spaces ZeroGPU: Dynamic GPU Allocation for Spaces β Official HF docs
- Make your ZeroGPU Spaces go brrr with ahead-of-time compilation β HF Blog, AoT compilation guide
- Disk usage on Spaces / Storage Buckets β HF Storage docs
- Schedule Jobs β HF Scheduled Jobs
- HF Inference Endpoints Pricing β HF Endpoints pricing
Community Sources
- ZeroGPU illegal duration discussion β Confirms 120s hard limit
- HF ZeroGPU Space Hangs β Debug patterns
- GPU Task aborted β Timeout behavior
- ZeroGPU quota discussion β Quota mechanics
- Debugging ZeroGPU Spaces (KB) β Comprehensive debug guide
Example Implementations
- cbensimon/zero-gpu-queue β Queue mechanics test
- LTX-Video Playground β Dynamic duration
- Wan 2.2 AoTI β Sophisticated duration estimation
- Agent Zero Orchestration β Multi-step agent on ZeroGPU
- ltx2.3-AIO-generator β Multi-mode video gen with presets
- spaces library source β ZeroGPU internals
Pricing Sources
- HostFleet Serverless GPU Pricing Matrix (April 2026) β Cross-vendor comparison
- RunPod Pricing β GPU cloud pricing
- Replicate Pricing β Per-second inference pricing
- HuggingFace Inference API 2026 β API pricing breakdown
Provenance
| Claim | Source | Confidence |
|---|---|---|
| 120s hard duration limit | HF Forums, official docs | High |
| Process killed (SIGKILL-like) at timeout | HF blog ("kills the fork"), community reports | High |
| Duration >120s sometimes works up to 240s | Community reports | Medium |
| Quota charged at reserved duration, not actual | HF Forums discussion | High |
| AoT gives 1.3β1.8Γ speedup | HF official blog post | High |
| FP8 gives additional 1.2Γ speedup | HF official blog post | High |
| Model weights persist across GPU calls | Official docs, spaces source | High |
| 10 ZeroGPU Spaces limit per Pro account | Official docs | High |
| Storage Buckets persist across Space restarts | Official docs | High |
| Pricing figures | Vendor pricing pages as of April 2026 | High |