Ten test images from a ComfyUI workflow take a few clicks. A thousand need a different setup: ComfyUI batch processing through the queue rather than the button, a script that talks to the ComfyUI API, one process per GPU, rented hardware when your own cards run out, and a plan for the outputs you will throw away.
Written from practice: we run client workflows at volume on our own GPUs and, for large orders, on rented ones from Vast.ai.
Batch size vs. queue: two ways to batch generate in ComfyUI
Two separate knobs that often get confused:
- Batch size is the
batch_sizeinput onEmpty Latent Image(1 to 4096). The sampler processes all those latents in one pass. - The queue is the batch count next to the Run button, or several API calls. Each run is a separate job, executed one after another. Models stay loaded between runs while memory allows.
| Batch size | Queue (separate runs) | |
|---|---|---|
| VRAM | grows with every latent; too large means out-of-memory | same as one run |
| Speed | can use the GPU better on small models and resolutions | small overhead per run once models are loaded |
| Seeds | one seed for the whole batch | its own seed per run |
| Reproducing one output | same seed and batch size, or LatentFromBatch | rerun with its seed |
| On error | the whole batch is lost | one output is lost |
ComfyUI generates noise for the whole batch from a single seed, so image 5 of a batch of 8 can only be reproduced as “seed X, batch of 8, index 4”. When you regenerate rejects later, separate runs with their own seeds are far easier.
Measure seconds per output, not per run. Raise the batch size on the actual card until VRAM is nearly full or the time per image stops dropping, then use the queue for volume. Video graphs almost always run one clip per job.
One trap: the UI seed widget’s control after generate setting (fixed, increment, randomize) is a frontend feature. Through the API the seed in the JSON is used as is, so your script must set it.
Prompts from a file, seed lists and input images
A thousand runs with the same prompt and seed is one output a thousand times; ComfyUI even caches an identical job and saves no new file. Volume comes from varying something:
- Seeds. A fixed list, or a start value plus the job index, so any output can be reproduced.
- A prompt matrix. Subject × style × background: 10 × 5 × 4 is already 200 combinations. Build it in a script.
- Prompts from a file. Custom node packs do this inside the UI; Inspire Pack, for example, has
Load Prompts From File. For large runs a script reading a text or CSV file is easier to control and resume. - Input images. For image-to-image, image-to-video or editing graphs, each input file is one job. Put files in ComfyUI’s
inputfolder (or upload them withPOST /upload/image) and set the file name in theLoadImagenode.
Keep a manifest: one row per job with prompt, seed, input, output and status. It lets you resume after a crash and rerun rejects with new seeds.
Run a ComfyUI workflow programmatically through the API
ComfyUI is a web server, and the browser is just one of its clients.
- Export in API format:
File → Export Workflow (API)(older versions: Save (API Format), visible after enabling dev mode options). The regular workflow JSON won’t be accepted. The API JSON maps node ids toclass_typeandinputs; a link like["4", 0]means output 0 of node 4. Note the ids of the nodes you will change. - Queue:
POST /promptwith{"prompt": <API JSON>}returns aprompt_id. A broken graph gets HTTP 400 withnode_errorsbefore anything is queued. - Wait:
GET /history/{prompt_id}returns an empty object until the job finishes, then the outputs and astatus. For live progress, openws://host:8188/ws?clientId=<id>and send the sameclient_idwith/prompt: you getprogress,executing(withnode: nullwhen done),execution_successandexecution_error. - Download: each file comes with
filename,subfolderandtype; pass them toGET /view.
A minimal script, standard library only: every line of prompts.txt with every seed, spread over two servers.
import copy, json, time, urllib.parse, urllib.request
from pathlib import Path
SERVERS = ["127.0.0.1:8188", "127.0.0.1:8189"] # one ComfyUI process per GPU
PROMPT_NODE, SEED_NODE = "6", "3" # node ids from your API-format JSON
SEEDS = [1001, 1002, 1003]
workflow = json.loads(Path("workflow_api.json").read_text())
prompts = [p.strip() for p in Path("prompts.txt").read_text().splitlines() if p.strip()]
out = Path("out")
out.mkdir(exist_ok=True)
def call(server, path, payload=None):
data = json.dumps(payload).encode() if payload is not None else None
req = urllib.request.Request(f"http://{server}{path}", data=data,
headers={"Content-Type": "application/json"})
with urllib.request.urlopen(req) as resp:
return json.loads(resp.read())
jobs = []
for i, text in enumerate(prompts):
for seed in SEEDS:
wf = copy.deepcopy(workflow)
wf[PROMPT_NODE]["inputs"]["text"] = text
wf[SEED_NODE]["inputs"]["seed"] = seed
server = SERVERS[len(jobs) % len(SERVERS)]
prompt_id = call(server, "/prompt", {"prompt": wf})["prompt_id"]
jobs.append((server, prompt_id, f"{i:04d}_{seed}"))
for server, prompt_id, tag in jobs:
while not (history := call(server, f"/history/{prompt_id}")):
time.sleep(2)
entry = history[prompt_id]
if entry["status"]["status_str"] != "success":
print("failed:", tag)
continue
for node_output in entry["outputs"].values():
files = [f for v in node_output.values() if isinstance(v, list)
for f in v if isinstance(f, dict) and f.get("type") == "output"]
for f in files:
query = urllib.parse.urlencode({k: f[k] for k in ("filename", "subfolder", "type")})
with urllib.request.urlopen(f"http://{server}/view?{query}") as resp:
(out / f"{tag}_{f['filename']}").write_bytes(resp.read())
6 and 3 are the prompt and KSampler nodes of ComfyUI’s default workflow; yours will differ, and some samplers call the field noise_seed. A job that fails validation raises HTTPError; its body holds the node_errors.
Notes from real runs:
- History lives in the ComfyUI process memory. After a restart the files remain, but the job-to-file mapping is gone. Write each
prompt_idto the manifest right away. - For thousands of jobs, queue in chunks and give the next job to the server with the shortest queue (
GET /queue). - ComfyUI has no login. Don’t expose its port; use an SSH tunnel or an authenticating proxy.
ComfyUI on multiple GPUs
One ComfyUI process generates on one GPU. For volume, run one process per card, each on its own port:
python main.py --cuda-device 0 --port 8188 --output-directory /data/out0 --temp-directory /data/tmp0
python main.py --cuda-device 1 --port 8189 --output-directory /data/out1 --temp-directory /data/tmp1
--cuda-devicemakes only that card visible to the process.- Separate output and temp folders: ComfyUI clears temp on startup and numbers files by scanning the output folder, so shared folders cause collisions.
- The models folder can be shared, but each process loads its own copy into VRAM and system RAM.
Since late May 2026 ComfyUI also has a built-in MultiGPU CFG Split node that splits one generation across identical GPUs. Per the documentation it needs identical Ampere-or-newer cards, works with a listed set of models (WAN 2.2 and SDXL among them) and gives nothing at CFG 1, which rules out distilled setups such as Lightning-type LoRAs. For a thousand outputs, separate processes are simpler, and one crash doesn’t stop the others.
More machines just mean more addresses in SERVERS, with the same ComfyUI version, node commits and models everywhere.
ComfyUI in the cloud vs. local: limits to check
Locally you control everything, but one or two cards set the ceiling. Hosted ComfyUI services and serverless GPU APIs remove the hardware question and add their own limits. Check:
- Custom nodes and models: often only an approved set of nodes; uploads of your own LoRA may be limited.
- Parallelism: how many jobs per account run at once. That is your throughput.
- Timeouts: the maximum time per job, which long video graphs can hit.
- Cold starts: on serverless, the first job after idle loads the models.
- Billing unit: GPU seconds, jobs or credits. It decides what your rejects cost.
Renting a whole GPU machine sits in between: any nodes and models, but the setup is yours.
ComfyUI on Vast.ai: what renting GPUs taught us
Vast.ai is a marketplace of GPU hosts; for large orders we run several instances there in parallel. Lessons that cost us time:
- The disk is fixed at creation. Vast doesn’t let you resize it later, and storage is billed even while the instance is stopped. A video graph’s models take tens of gigabytes; our rule of thumb is about 1.5 times the total model weight plus room for outputs, never below 50 GB.
- A cold start takes minutes, and you pay for them. Container start, ComfyUI and custom node install, then the model download, usually the longest part and dependent on the host network. One instance should take a long share of the run.
- Hosts are not all the same. Our video graph upscales with NVIDIA RTX Video Super Resolution, and on some hosts that node would not initialize although CUDA worked. Now we test such nodes right after boot, before downloading the models, and switch hosts if they fail. Vast’s reliability score is a filter, not a guarantee.
- Pin versions. Custom nodes from git at fixed commits, and ComfyUI pinned per workflow. We had a model loader fail because the pinned ComfyUI commit predated support for that model. Unpinned installs eventually change the output mid-run.
- Assume the instance can disappear. Copy outputs off the machine as you go (we use cloud storage) and keep the manifest elsewhere.
Quality control at volume
At a thousand outputs the question is what share is unusable and who finds it.
- Trial batch first: 20 to 50 outputs drawn from across the prompt matrix and inputs, not the first 50 rows. We agree it with the client before the volume run.
- Measure the reject rate. If one in five fails, generate about 1,250 for 1,000 good ones, and rework the worst prompts.
- Freeze the setup. Changing models or parameters mid-run means a new trial batch.
- Review by eye: hands and faces, artifacts, broken text, character drift, flicker and seams in video. We review manually, in a grid sorted by prompt, so a bad prompt shows up as a block.
- Regenerate rejects from the manifest with new seeds.
What 1,000 generations really cost
GPU cost = t × N ÷ (1 − r) ÷ 3600 × P
Total cost ≈ GPU cost + setup + trial batch + review
t is seconds per output, N the good outputs you need, r the reject share, P the GPU hour price.
tcan’t be borrowed from someone else’s benchmark: it depends on model, resolution, steps, graph and card. Measure it on the trial batch.- Several GPUs shorten wall-clock time, not GPU hours.
- Setup is cold starts times instances, plus hours of installing and debugging. At small volumes it dominates.
- Review is human time and grows with
N.
Our price for images starts at $0.10 per image on any checkpoint and LoRA. Setup and the trial batch are included; the minimum order is $50.
When it’s easier to hand it to us
Have a working workflow and need volume, not a side job in GPU operations? Send it to us. We install the template as is, with nodes at pinned commits and models from Hugging Face, Civitai or a direct link, make a trial batch for your approval, run the volume on our own or rented GPUs, review by hand and deliver an archive or a folder link. If a model is paid, closed or its license forbids commercial use, we say so before the start (see Civitai licenses).
- Your workflow at volume: ComfyUI workflow service
- Images on your checkpoint or LoRA: bulk image generation
- The workflow doesn’t start yet: how to run a Civitai workflow
Send the workflow, the task and the volume through the form below.