Automate Batch Image Generation with the ComfyUI API

"The official ComfyUI Server API documentation describes /prompt validation and queueing plus the core /ws, /history, /view, /queue, and /interrupt routes."
You already have a stable ComfyUI workflow and need four hero images for each of 200 products. Sitting in the interface to change the prompt and seed for every run is not realistic. Your first script POST to /prompt returns node_errors because the exported file is not in API format. Then the job finishes, but /history contains only a filename and subfolder, and it is not obvious that /view is required to download the image.
Moving ComfyUI from GUI work to scripted production involves three hurdles: exporting the correct API-format workflow, waiting for completion over WebSocket instead of polling blindly, and controlling concurrency and VRAM to avoid OOM errors. The sections below provide a minimal script, parameterized batch examples, queue strategies, and a backend checklist for handling format errors, confusing job states, memory pressure, and missing outputs.
Local ComfyUI Server API Entry Points
CLI Startup and the Default Port
The local ComfyUI service listens on 127.0.0.1:8188 by default. Start it normally when only local processes need access. Specify a listen address only when another device on the LAN must connect:
# Local access only
python main.py --port 8188
# Allow LAN access; also configure a firewall, authentication, or a reverse proxy
python main.py --listen 0.0.0.0 --port 8188
When the terminal prints Starting server, open http://127.0.0.1:8188. Seeing the ComfyUI frontend confirms that the service is ready.
When VRAM is tight, use --lowvram, --novram, or, as a slow last resort, --cpu if the current version supports them. With ample VRAM, --highvram may also be worth testing. Do not map these flags mechanically to a fixed GPU capacity: the model, precision, VAE, and workflow structure all affect the peak. Check python main.py --help and the current official troubleshooting documentation before choosing flags.
Core API Routes
The main local ComfyUI Server routes are defined in server.py. These are the endpoints a script commonly needs:
| Route | Purpose | Parameters/response |
|---|---|---|
/prompt | Validate a workflow and add it to the queue | POST {"prompt": workflow_dict, "client_id": "..."}; returns prompt_id on success and node_errors on failure |
/history/{prompt_id} | Retrieve job history and output metadata | GET; returns outputs with filename, subfolder, and type |
/view?filename=...&subfolder=...&type=... | Download an output file | GET; returns binary data |
/ws | Receive execution status over WebSocket | ws://127.0.0.1:8188/ws?clientId=... |
/queue | Inspect the current queue | GET; returns queued jobs |
/interrupt | Interrupt the current execution | POST; useful for timeout handling |
/upload/image | Upload an input image for image-to-image workflows | POST multipart/form-data |
/object_info | Query available node types and parameters | GET; useful for checking whether a node exists |
Routes can be added or changed between ComfyUI versions. Check the current documentation or server.py when behavior differs.
WebSocket Message Types
Watch these message types while waiting for a job:
status: queue status, includingqueue_remainingexecution_start: execution has started and includes theprompt_idexecution_cached: lists nodes reused from cacheexecuting: identifies the current node;node is Nonewith a matchingprompt_idmeans the job has finishedprogress: current and total steps for a long-running nodeexecuted: a node has completed and includes output metadata
The completion check is straightforward: receive a message whose type is "executing", verify that data.node is None, and confirm that data.prompt_id matches the ID returned for the submitted job.
Export an API-Format Workflow
Export Workflow (API)
The workflow JSON saved by the ComfyUI frontend is not the same representation expected by the API. Posting an ordinary workflow can fail validation with node_errors, so export the API format first.
Export it as follows:
- Load a workflow that already generates a valid image in the ComfyUI frontend
- Choose
File -> Export Workflow (API); some versions may showSave (API Format), so follow the wording in the current interface - Save the result as a
.jsonfile such asworkflow_api.json - Open the JSON and confirm that nodes use numeric IDs such as
"3"and"6", withclass_typeandinputsin each node
Menu labels may change with frontend updates, so use the equivalent API export command in the version you are running.
API Format vs. Save Format
A regular Save-format workflow includes frontend layout data. The API format removes that metadata and keeps the information required for execution:
| Format | Contents | Use |
|---|---|---|
| Save format | Node positions, colors, groups, dimensions, and visual link data | Reopen and edit the layout in the frontend |
| API format | Numeric node IDs, class_type, inputs, and optional _meta | Submit the workflow through a script or API |
Posting Save-format JSON directly to /prompt can return node_errors or error. Load it in the frontend and export it again in API format.
Managing Node IDs
Parameterization requires the IDs of the prompt, seed, dimension, and output nodes. Inspect the exported API-format JSON and locate:
- Prompt node:
CLIPTextEncode, often"6"in simple examples - Seed node:
KSampler, often"3"in simple examples - Width/height node: usually an input on
EmptyLatentImageor another workflow-specific node - Output node:
SaveImageorSaveImageWebsocket
Notes or groups in the frontend can document each node’s purpose, but the script still needs to inspect the exported JSON. IDs are generated for a specific graph and can change after re-export, so never assume that a node name implies a fixed ID.
Minimal Script: Submit, Wait, and Download
The API flow has three steps: submit a workflow to /prompt, wait for it to finish, then retrieve metadata through /history and download files through /view.
HTTP Submit-and-Forget
The smallest client posts the workflow to /prompt and does not wait. This works when a separate worker polls the resulting jobs.
import json
import requests
# Load an API-format workflow
with open("workflow_api.json", "r") as f:
workflow = json.load(f)
# Build the request payload
payload = {
"prompt": workflow,
"client_id": "my-script-client" # Optional; associates the job with WebSocket events
}
# Add the job to the queue
response = requests.post("http://127.0.0.1:8188/prompt", json=payload)
if response.status_code == 200:
result = response.json()
prompt_id = result["prompt_id"]
print(f"Submitted: {prompt_id}")
else:
error = response.json()
print(f"Submission failed: {error}")
# node_errors contains node-level validation details
A successful response includes {"prompt_id": "...", "number": ...}. A failed one contains {"error": {...}, "node_errors": {...}}. Submission only adds the job to the queue; it does not wait for completion.
Wait over WebSocket
WebSocket events avoid aggressive polling. Connect first, submit the job, and then wait for the terminal executing event.
import json
import uuid
import requests
import websocket
# Load the workflow
with open("workflow_api.json", "r") as f:
workflow = json.load(f)
# Generate a client ID
client_id = str(uuid.uuid4())
# Connect to WebSocket
ws = websocket.create_connection(f"ws://127.0.0.1:8188/ws?clientId={client_id}")
# Submit the job
payload = {"prompt": workflow, "client_id": client_id}
response = requests.post("http://127.0.0.1:8188/prompt", json=payload)
prompt_id = response.json()["prompt_id"]
# Wait for completion
while True:
message = ws.recv()
data = json.loads(message)
if data["type"] == "executing":
# node is None with the same prompt_id when the job is complete
if data["data"]["node"] is None and data["data"]["prompt_id"] == prompt_id:
print("Job complete")
break
ws.close()
Add a maximum wait time, such as 300 seconds. If the timeout expires, call /interrupt with care because it stops the current execution.
Download Images with History and View
After completion, request /history/{prompt_id} for output metadata, then download each actual file through /view.
import requests
# Retrieve job history
history_url = f"http://127.0.0.1:8188/history/{prompt_id}"
history = requests.get(history_url).json()
# Traverse output nodes
outputs = history[prompt_id]["outputs"]
for node_id, node_output in outputs.items():
if "images" in node_output:
for image in node_output["images"]:
filename = image["filename"]
subfolder = image.get("subfolder", "")
type = image.get("type", "output")
# Build the download URL
view_url = f"http://127.0.0.1:8188/view?filename={filename}&subfolder={subfolder}&type={type}"
# Download the binary image
img_data = requests.get(view_url).content
with open(f"output_{filename}", "wb") as f:
f.write(img_data)
print(f"Saved: output_{filename}")
The outputs object is organized as {node_id: {"images": [{"filename": "...", "subfolder": "...", "type": "..."}]}}. History provides metadata; /view returns the binary image.
Parameterize Batch Jobs
Parameterization turns a stable workflow into a batch template. Each iteration changes a prompt, seed, dimension, or other selected input instead of requiring GUI edits.
Parameterize Prompt, Seed, and Dimensions
Change values under a node’s inputs:
import json
import random
# Load the workflow
with open("workflow_api.json", "r") as f:
workflow = json.load(f)
# Change the prompt; use the IDs from your own workflow
workflow["6"]["inputs"]["text"] = "a beautiful landscape, sunset, mountains"
# Generate a random seed
workflow["3"]["inputs"]["seed"] = random.randint(0, 1000000)
# Change the dimensions
workflow["5"]["inputs"]["width"] = 1024
workflow["5"]["inputs"]["height"] = 768
# Submit the job...
The IDs "6", "3", and "5" must be confirmed in the actual exported workflow. A loop can then update the prompt and seed before each submission:
prompts = [
"product photo, white background",
"product photo, outdoor scene",
"product photo, studio lighting"
]
for i, prompt_text in enumerate(prompts):
workflow["6"]["inputs"]["text"] = prompt_text
workflow["3"]["inputs"]["seed"] = random.randint(0, 1000000)
# Submit the job
payload = {"prompt": workflow, "client_id": client_id}
response = requests.post("http://127.0.0.1:8188/prompt", json=payload)
prompt_id = response.json()["prompt_id"]
# Wait over WebSocket...
# Download outputs...
The official examples also modify KSampler.seed and CLIPTextEncode.text, which makes them useful starting points for locating parameters.
Batch Queue Strategies
Generating 200 images can mean either increasing a workflow’s batch size or submitting many prompts. The safer choice depends on the model and measured VRAM headroom.
| Strategy | Characteristics | Suitable cases |
|---|---|---|
| One image per submission | Easier to control VRAM per job | Limited VRAM headroom, large models, or complex workflows |
| Batch size | Produces several images inside one prompt and usually raises peak VRAM | Ample measured VRAM headroom, smaller models, and simple workflows |
| Concurrent requests | Submits several prompts while limiting active work | Higher throughput with measured capacity and controlled workers |
Submitting 20 prompts at once can fill the queue and push the runtime into an OOM error or crash.
The conservative default is sequential submission, WebSocket completion, and VRAM monitoring. Submit the next job only after the previous one finishes. If memory remains a problem, reduce workload or test current low-memory options rather than hiding the issue with retries.
Concurrency Control Example
On a multi-GPU server or several isolated cloud workers, a semaphore can cap active jobs:
import copy
import threading
# Allow at most two active jobs
semaphore = threading.Semaphore(2)
def submit_and_wait(prompt_text, seed):
with semaphore:
# Give each task its own copy instead of mutating shared state
job_workflow = copy.deepcopy(workflow)
job_workflow["6"]["inputs"]["text"] = prompt_text
job_workflow["3"]["inputs"]["seed"] = seed
# Submit and wait...
# WebSocket loop...
# The permit is returned for the next task
# Submit the batch
threads = []
for i in range(50):
t = threading.Thread(target=submit_and_wait, args=(prompts[i], seeds[i]))
threads.append(t)
t.start()
for t in threads:
t.join()
Start at concurrency 1. Increase it only after measuring peak memory, latency, and failures with the real workflow. Two GPUs with the same capacity can still behave differently because model precision, VAE, post-processing nodes, and worker isolation affect the limit.
Monitor VRAM
During a batch, query system statistics and pause new submissions above a chosen threshold:
import time
def check_vram(threshold=0.8):
stats = requests.get("http://127.0.0.1:8188/system_stats").json()
vram_used = stats["system_stats"]["devices"][0]["vram_used"]
vram_total = stats["system_stats"]["devices"][0]["vram_total"]
return (vram_used / vram_total) > threshold
# Check memory before each submission
for prompt_text in prompts:
while check_vram(0.85):
print("VRAM pressure is high; waiting 30 seconds...")
time.sleep(30)
# Submit the next job...
# Wait over WebSocket...
Peak memory often occurs while KSampler runs, and utilization can fall after completion. Poll every 10–30 seconds rather than flooding the service.
Archive Batch Outputs
Batch output needs a predictable layout. A useful filename is {prompt_id}_{seed}_{timestamp}.png, which preserves the task ID, random seed, and creation time.
Record at least:
prompt_id: the ComfyUI task IDseed: the random seedprompt_text: the prompt used for the imagewidth/height: output dimensionstimestamp: generation timebusiness_id: an application identifier such as a product SKU or order number
Organize directories by date or batch, for example outputs/20260624/batch_001/. SQLite or PostgreSQL can map metadata to image paths when filesystem records are no longer enough.
Backend Engineering Checklist
A backend wrapper needs request IDs, timeouts, queue limits, memory monitoring, and explicit error handling. A working script alone is not a reliable production service.
Request IDs and Idempotency
Generate a unique business request_id, such as a UUID or order number, and map it to the ComfyUI prompt_id. If a completed request_id arrives again, return the existing result rather than generating another image.
import uuid
# Business request ID
request_id = str(uuid.uuid4())
# Store the mapping in a database or cache
request_prompt_map[request_id] = prompt_id
# Return an existing result for duplicate requests
if request_id in completed_requests:
return get_cached_result(request_id)
ComfyUI creates the prompt_id; it is not the same as the application’s request_id. Persist the mapping yourself.
Timeouts and Cancellation
Set a maximum execution time, such as 300 seconds. When it expires, call /interrupt to stop the current execution and allow the queue to continue.
import time
timeout = 300 # Five minutes
start_time = time.time()
# Wait for WebSocket messages...
while True:
elapsed = time.time() - start_time
if elapsed > timeout:
# Interrupt the current execution
requests.post("http://127.0.0.1:8188/interrupt")
print("Job timed out and was interrupted")
break
# Process normal messages...
If WebSocket disconnects or remains silent beyond a chosen limit, reconnect or fall back to history. After an interruption, inspect /queue and decide whether pending jobs should be cleared.
Queue Limits and VRAM Monitoring
Set an explicit queue ceiling, such as five pending jobs, and reject or defer new requests above it. Also cap active workers so several requests cannot create an unexpected memory peak.
Query /system_stats to inspect VRAM:
stats = requests.get("http://127.0.0.1:8188/system_stats").json()
vram_used = stats["system_stats"]["devices"][0]["vram_used"]
vram_total = stats["system_stats"]["devices"][0]["vram_total"]
vram_percent = vram_used / vram_total
if vram_percent > 0.8:
print("VRAM pressure is high; rejecting a new request")
When pressure is high, reject new work or wait for the queue to drain. Reducing batch size and simplifying the workflow should come before repeated retries.
Error Classification and Retries
Different failures need different responses:
| Error | Likely cause | Handling |
|---|---|---|
node_errors | Missing model or node, invalid parameter, or missing input | Fix the workflow and environment; do not retry |
| OOM | Insufficient VRAM | Reduce batch size or workload and change a verified memory option; do not retry unchanged |
| WebSocket disconnect | Network or connection problem | Reconnect and query /history/{prompt_id} |
| Job timeout | Slow model loading or a complex workflow | Increase the timeout or simplify the workflow; retry at most once |
| Service crash | Exhausted memory or GPU failure | Inspect logs and restart the service; retry cautiously |
node_errors identifies the nodes that failed validation, including missing node classes and mismatched input types. Repair the workflow, custom-node installation, or model files first. An OOM error also needs a memory change, not an immediate identical retry.
Compare Cloud and Local APIs
Comfy Cloud provides hosted programmatic workflow execution, but authentication, job status, WebSocket addresses, and concurrency differ from the local Server API. The API-format workflow concept carries over; endpoint code still needs to follow the current Cloud API Reference.
Route Differences
The main route differences are:
| Function | Local API | Cloud API |
|---|---|---|
| Submit a job | /prompt | /api/prompt |
| Check status/results | /history/{prompt_id} | /api/job/{prompt_id}/status and /api/jobs/{job_id} |
| Download output | /view | /api/view |
| WebSocket | /ws?clientId=... | /ws?clientId=...&token=... |
Cloud requires an X-API-Key header and an active subscription. The local API listens only on 127.0.0.1 by default, but exposing it through --listen, a reverse proxy, or port forwarding makes authentication and access controls your responsibility. Cloud documentation still marks the API experimental. The older /api/history_v2/{prompt_id} endpoint is deprecated in favor of /api/jobs/{job_id}, so verify the current reference before migration.
Concurrency and Subscription Limits
Cloud concurrency depends on the subscription tier. Jobs above that tier’s parallel limit wait in a queue. Outputs live in cloud storage, and /api/view returns a temporary signed URL.
Plans, concurrency, runtime limits, and pricing can change, so this guide does not hard-code numbers. Cloud avoids local GPU management, while the local Server API leaves queueing, VRAM, security, and service stability to you.
Next Steps and Further Reading
Within the Series
This article is the engineering step from a working visual workflow to programmable batch production and backend integration:
- Reuse ComfyUI Workflows: import workflows, fix missing nodes, and resolve model paths
- ComfyUI low-VRAM and performance tuning: memory options, batching, OOM diagnosis, and tiled VAE
- ComfyUI video generation: where image batch automation stops and video-specific workflows begin
- ComfyUI troubleshooting and maintenance: missing nodes, startup failures, version conflicts, and runtime repair
Across Related Topics
For broader automation and backend patterns:
- Build AI Workflows with n8n: connect ComfyUI to multi-tool automation
- Ollama API practice: programmatic model calls, queues, and structured outputs
- Structured LLM output: reliable data extraction and API integration
Conclusion
The path from ComfyUI GUI work to scripted production is: export an API-format workflow, wait for the job over WebSocket, download files through /history and /view, parameterize selected inputs, then add request IDs, timeouts, queue limits, and VRAM monitoring.
Start by exporting one API-format workflow and running the minimal submit-and-wait script. Once a single image works, generate ten parameterized images and observe memory and queue behavior. Add idempotency, cancellation, monitoring, and result records only after that small batch is stable.
Run Your First Batch Job with the ComfyUI API
Validate the local automation path from workflow export through output archiving.
- 1
Step 1: Validate the GUI workflow
Generate one image reliably in the ComfyUI frontend and confirm that the models, custom nodes, input files, and output nodes all work. - 2
Step 2: Export API format
Use the current interface's Export Workflow (API) command and confirm that every node object contains class_type and inputs. - 3
Step 3: Submit one job
POST the workflow in the prompt field to /prompt, save the returned prompt_id, and inspect node_errors if validation fails. - 4
Step 4: Wait and download
Listen on /ws for the completion message for that prompt, or poll /history/{prompt_id}, then download each output through /view. - 5
Step 5: Parameterize a few inputs
Copy the workflow template, change prompt, seed, width, height, and filename_prefix one at a time, and validate the node ID and class_type. - 6
Step 6: Add batch safeguards
Start with sequential submissions, then add a business job_id, idempotency, queue limits, timeouts, VRAM monitoring, error classification, and output archiving.
FAQ
Which workflow JSON should I use with the ComfyUI API?
What is the minimum local ComfyUI API flow?
What does node_errors mean?
Can I still retrieve results after WebSocket disconnects?
Should batch generation use a larger batch size or repeated submissions?
Can multiple prompts be submitted concurrently?
Are Comfy Cloud and the local API identical?
14 min read · Published on: Jul 24, 2026 · Modified on: Jul 24, 2026
ComfyUI & Stable Diffusion: Setup, Models, and Workflows
If you landed here from search, the fastest way to build context is to jump to the previous or next post in this same series.
Previous
ComfyUI Video Generation: Wan Text-to-Video, Image-to-Video, and AnimateDiff
Run your first ComfyUI video workflow with Wan or AnimateDiff, covering model folders, custom nodes, frames, FPS, VRAM limits, export, and troubleshooting.
Part 9 of 10
Next
This is the latest post in the series so far.



Comments
Sign in with GitHub to leave a comment