Download System¶
ComfyUI Studio includes a thread-pool-based download manager implemented in backend/download.py. It handles downloading models from HuggingFace and CivitAI with concurrent execution, progress tracking, and automatic authentication.
Architecture¶
User clicks "Download"
│
▼
_enqueue_download(model)
│
▼
_pending_queue ←── ordered FIFO list
│
▼
_schedule_downloads() ←── runs under _queue_lock
│ submits up to _max_concurrent threads
▼
ThreadPoolExecutor (max_workers=10)
│
▼
_do_download(item) ←── one thread per active download
│
├── _get_download_url(model) ← construct URL from catalog fields
├── _inject_auth(url) ← add HF_TOKEN or CIVITAI_API_KEY
├── requests.get(stream=True) ← 8 MB chunks
├── write to {dest}/{file}.tmp
└── rename to {dest}/{file} ← atomic on success
Concurrency Control¶
Max Concurrent Downloads¶
The maximum number of simultaneous downloads defaults to 3 and is controlled by the MAX_CONCURRENT_DOWNLOADS environment variable:
This value can also be changed at runtime from the Settings page without restarting the backend.
The ThreadPoolExecutor itself has 10 worker threads, but _schedule_downloads() enforces the concurrency limit by only submitting tasks when _active_count < _max_concurrent. When a download finishes (success or error), _on_download_complete() decrements the active count and tries to schedule the next pending download.
Queue Behavior¶
Downloads that exceed the concurrent limit are held in _pending_queue (a Python list, FIFO order). They appear with status "queued" in the API response. As active downloads complete, queued items are promoted automatically.
All queue operations are protected by _queue_lock (a threading.Lock) to prevent race conditions between the API thread and download threads.
Download State¶
In-Memory State Dict¶
Download progress is tracked in _download_state, a module-level dict keyed by filename:
_download_state = {
"wan2.2_i2v_high_noise_14B_fp8_scaled.safetensors": {
"status": "downloading", # "queued" | "downloading" | "done" | "error"
"bytes": 5_368_709_120, # bytes written so far
"total": 13_300_000_000, # Content-Length from server (0 if unknown)
"speed": 125_000_000.0, # bytes/second (rolling estimate)
"error": None, # error message string, or None
"_last_bytes": 5_100_000_000, # internal: bytes at last speed sample
"_last_time": 1711234567.89, # internal: time of last speed sample
}
}
Fields prefixed with _ are internal and stripped by _clean_state() before being sent to the frontend.
State Lifecycle¶
- Not in dict: model has never been downloaded, or state was cleared after deletion
- Queued:
_enqueue_download()was called, waiting for a download slot - Downloading: thread is actively downloading,
bytesandspeedupdate in real time - Done: download completed successfully, file renamed from
.tmpto final name - Error: download failed,
.tmpfile cleaned up,errorfield contains the reason
State Persistence¶
Download state is in-memory only. If the backend restarts, all state is lost -- queued and in-progress downloads are forgotten. Partially downloaded .tmp files are left on disk but are not resumed. This is a known limitation; SQLite-backed state is planned for a future release.
Download Flow (Detail)¶
1. Enqueue¶
When the user clicks download on a model card, the frontend calls:
The endpoint:
- Calls
_reload_models()to get fresh catalog data - Finds the model entry by filename using
_find_model() - Checks if the model is already downloading or queued -- if so, returns current status
- Calls
_enqueue_download(model)which sets state to"queued"and appends to_pending_queue _schedule_downloads()immediately tries to start the download if a slot is available
2. URL Construction¶
_get_download_url(model) constructs the URL from catalog fields:
- If
civitai_version_idis present:https://civitai.com/api/download/models/{id} - If
hf_repoandhf_fileare present:https://huggingface.co/{repo}/resolve/main/{file} - Otherwise: falls back to a legacy
urlfield (not used in current catalogs)
3. Auth Injection¶
_inject_auth(url) adds authentication without modifying the catalog:
- HuggingFace: adds
Authorization: Bearer {HF_TOKEN}header (ifHF_TOKENenv var is set) - CivitAI: appends
?token={CIVITAI_API_KEY}query parameter (ifCIVITAI_API_KEYenv var is set)
Returns a tuple of (modified_url, headers_dict).
4. Streaming Download¶
_do_download(item) runs in a thread pool thread:
- Determines the destination directory:
ComfyUI/models/{dest}/(orSTUDIO_DIR/llm/models/for LLM models) - Creates the directory if it does not exist (
mkdir -p) - Opens a streaming
requests.getwith 60-second timeout and redirect following - Reads
Content-Lengthheader to settotalbytes - Writes data in 8 MB chunks to a
.tmpfile - Updates
bytesin_download_stateafter every chunk - Recalculates
speedapproximately every 1 second using a rolling window:(current_bytes - last_sample_bytes) / elapsed_seconds - On success: atomically renames
.tmpto final filename, sets status to"done" - On error: deletes the
.tmpfile, sets status to"error"with the exception message
5. Events¶
The download system emits events through the EventBus:
| Event | When | Severity |
|---|---|---|
model.download.completed |
Download finished successfully | success |
model.download.failed |
Download failed with an error | error |
These events appear in the activity panel on all pages and are logged to events.jsonl.
Batch Download¶
The batch download endpoint accepts multiple filenames at once:
POST /api/admin/models/download-batch
Content-Type: application/json
{
"filenames": [
"wan2.2_i2v_high_noise_14B_fp8_scaled.safetensors",
"wan2.2_i2v_low_noise_14B_fp8_scaled.safetensors",
"umt5_xxl_fp8_e4m3fn_scaled.safetensors"
]
}
Response:
Models are skipped if they are not found in the catalog or are already downloading/queued. Each successfully queued model goes through the same _enqueue_download() path as individual downloads.
Progress Monitoring¶
SSE Endpoint¶
The Models page subscribes to a Server-Sent Events stream for real-time download progress:
This returns an infinite SSE stream that emits a JSON payload every second:
{
"downloads": {
"wan2.2_i2v_high_noise_14B_fp8_scaled.safetensors": {
"status": "downloading",
"bytes": 5368709120,
"total": 13300000000,
"speed": 125000000.0,
"error": null
}
},
"timestamp": 1711234567.89
}
The frontend uses this to render progress bars, speed indicators, and status badges on model cards.
Catalog List Response¶
The main GET /api/admin/models endpoint also includes download state merged into each model entry. The response stats object provides aggregate information:
| Stat | Description |
|---|---|
queued_count |
Number of models waiting to download |
downloading_count |
Number of active downloads |
global_speed |
Combined download speed across all active downloads (bytes/sec) |
present_count |
Number of models found on disk |
total_count |
Total models in catalog |
models_bytes |
Total disk space used by present models |
free_bytes |
Estimated free space on the volume |
Model Deletion¶
When a model is deleted via DELETE /api/admin/models/{filename}:
- The file is removed from disk (
ComfyUI/models/{dest}/{filename}) - If the model has a download state of
"error"or"done", the state entry is cleared - Active downloads or queued downloads are not cancelled -- the state remains
- A
model.deletedevent is emitted
The model remains in the catalog after deletion. It simply shows as "missing" status and can be re-downloaded.
Environment Variables¶
| Variable | Default | Description |
|---|---|---|
MAX_CONCURRENT_DOWNLOADS |
3 |
Maximum simultaneous downloads |
HF_TOKEN |
"" |
HuggingFace access token for gated models |
CIVITAI_API_KEY |
"" |
CivitAI API key for model downloads and metadata |