Toggle Theme

Automate Batch Image Generation with the ComfyUI API

Easton editorial illustration: large dark node-graph workspace with orange connected nodes

"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:

RoutePurposeParameters/response
/promptValidate a workflow and add it to the queuePOST {"prompt": workflow_dict, "client_id": "..."}; returns prompt_id on success and node_errors on failure
/history/{prompt_id}Retrieve job history and output metadataGET; returns outputs with filename, subfolder, and type
/view?filename=...&subfolder=...&type=...Download an output fileGET; returns binary data
/wsReceive execution status over WebSocketws://127.0.0.1:8188/ws?clientId=...
/queueInspect the current queueGET; returns queued jobs
/interruptInterrupt the current executionPOST; useful for timeout handling
/upload/imageUpload an input image for image-to-image workflowsPOST multipart/form-data
/object_infoQuery available node types and parametersGET; 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, including queue_remaining
  • execution_start: execution has started and includes the prompt_id
  • execution_cached: lists nodes reused from cache
  • executing: identifies the current node; node is None with a matching prompt_id means the job has finished
  • progress: current and total steps for a long-running node
  • executed: 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:

  1. Load a workflow that already generates a valid image in the ComfyUI frontend
  2. Choose File -> Export Workflow (API); some versions may show Save (API Format), so follow the wording in the current interface
  3. Save the result as a .json file such as workflow_api.json
  4. Open the JSON and confirm that nodes use numeric IDs such as "3" and "6", with class_type and inputs in 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:

FormatContentsUse
Save formatNode positions, colors, groups, dimensions, and visual link dataReopen and edit the layout in the frontend
API formatNumeric node IDs, class_type, inputs, and optional _metaSubmit 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 EmptyLatentImage or another workflow-specific node
  • Output node: SaveImage or SaveImageWebsocket

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.

StrategyCharacteristicsSuitable cases
One image per submissionEasier to control VRAM per jobLimited VRAM headroom, large models, or complex workflows
Batch sizeProduces several images inside one prompt and usually raises peak VRAMAmple measured VRAM headroom, smaller models, and simple workflows
Concurrent requestsSubmits several prompts while limiting active workHigher 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 ID
  • seed: the random seed
  • prompt_text: the prompt used for the image
  • width/height: output dimensions
  • timestamp: generation time
  • business_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:

ErrorLikely causeHandling
node_errorsMissing model or node, invalid parameter, or missing inputFix the workflow and environment; do not retry
OOMInsufficient VRAMReduce batch size or workload and change a verified memory option; do not retry unchanged
WebSocket disconnectNetwork or connection problemReconnect and query /history/{prompt_id}
Job timeoutSlow model loading or a complex workflowIncrease the timeout or simplify the workflow; retry at most once
Service crashExhausted memory or GPU failureInspect 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:

FunctionLocal APICloud 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

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. 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. 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. 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. 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. 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. 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?
Use an API-format workflow. A regular Save-format file is mainly for editing in the frontend and includes layout data; load the workflow in ComfyUI and export it with Export Workflow (API).
What is the minimum local ComfyUI API flow?
POST the workflow to /prompt and save the prompt_id, wait through WebSocket or /history/{prompt_id}, then call /view with the filename, subfolder, and type returned by history.
What does node_errors mean?
It contains node-level validation failures from /prompt. Common causes include missing nodes or models, wrong parameter types, and missing required inputs. Fix the workflow or runtime instead of retrying blindly.
Can I still retrieve results after WebSocket disconnects?
Yes. Keep the prompt_id, reconnect if needed, or query /history/{prompt_id} and /queue directly. WebSocket provides live status but should not be the only record of a job.
Should batch generation use a larger batch size or repeated submissions?
For most local single-GPU setups, batch 1 with repeated submissions is the safer starting point. A larger batch usually raises peak VRAM, while a queue makes failures, retries, and output archiving easier to manage.
Can multiple prompts be submitted concurrently?
Yes, but start at concurrency 1, give each job an independent workflow copy, and increase only after measuring the real model, peak VRAM, queue length, and failure rate.
Are Comfy Cloud and the local API identical?
No. Both can use API-format workflows, but Cloud requires an X-API-Key and a subscription, and its job-status, WebSocket, and result endpoints differ. The official documentation still marks the Cloud API experimental.

14 min read · Published on: Jul 24, 2026 · Modified on: Jul 24, 2026

Comments

Sign in with GitHub to leave a comment

Easton BlogEaston Blog