惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

酷 壳 – CoolShell
酷 壳 – CoolShell
Microsoft Security Blog
Microsoft Security Blog
Recent Announcements
Recent Announcements
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Last Week in AI
Last Week in AI
罗磊的独立博客
腾讯CDC
云风的 BLOG
云风的 BLOG
月光博客
月光博客
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
博客园 - 三生石上(FineUI控件)
宝玉的分享
宝玉的分享
U
Unit 42
I
InfoQ
D
DataBreaches.Net
Blog — PlanetScale
Blog — PlanetScale
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
V
V2EX
美团技术团队
IT之家
IT之家
Stack Overflow Blog
Stack Overflow Blog
F
Fortinet All Blogs
GbyAI
GbyAI
S
SegmentFault 最新的问题

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant
ComfyUI API Tutorial 2026: Automate Image Generation
Jovan Chan · 2026-06-14 · via DEV Community

Jovan Chan

This article was originally published on aifoss.dev

TL;DR: ComfyUI's built-in HTTP server accepts workflow JSON on port 8188 — the same JSON the GUI exports when you click "Save (API Format)". You can queue a prompt, poll for completion, and download the result in under 50 lines of Python. No GUI, no browser tab, no manual clicking.

What you'll have running after this guide:

  • A Python function that submits any ComfyUI workflow and blocks until images are downloaded locally
  • A batch script that generates 100 images with automated seed variation and organized output folders
  • Working API-format JSON for an SDXL baseline workflow and a Flux.1 Schnell workflow you can adapt immediately

Why drive ComfyUI from the API

The GUI is fine for building and testing workflows. It's a problem once you need more than a few images, or need to integrate generation into a pipeline.

Common use cases where the API makes sense:

  • Batch generation — 50–1000 images with systematic prompt or seed variation
  • CI pipelines — trigger image generation on a new product SKU, game asset, or dataset update
  • Headless servers — GPU box in a closet or a RunPod cloud instance where running a browser makes no sense
  • App backends — a FastAPI endpoint that accepts user prompts and returns generated images

If you're generating one image at a time and want to tweak nodes visually, stay in the GUI. The API is for automation.


Prerequisites

  • ComfyUI v0.23.0 (latest as of June 2026) installed and working in the GUI
  • Python 3.10+
  • pip install requests websocket-client
  • At least one working workflow in your ComfyUI GUI

If you haven't installed ComfyUI yet, the ComfyUI review covers installation from scratch. For GPU hardware requirements, the Stable Diffusion 8GB VRAM guide has a good breakdown of what each model tier needs.


Step 1: Start ComfyUI with API access enabled

By default, ComfyUI only listens on 127.0.0.1 — accessible only from the same machine. That's fine if you're scripting locally. For a remote GPU box or Docker container, add --listen:

# Local access only (same machine)
python main.py --port 8188

# All interfaces (for remote scripting or Docker)
python main.py --listen 0.0.0.0 --port 8188

# With VRAM optimization for 8–12 GB cards
python main.py --listen 0.0.0.0 --port 8188 --lowvram

Once running, verify it's up:

curl http://127.0.0.1:8188/system_stats
# {"system": {"os": "posix", "python_version": "3.10.x", ...}}

The server starts a REST API and a WebSocket server on the same port. There is no authentication by default — if you expose this to a network, use a firewall rule or reverse proxy with auth.


Step 2: Export your workflow in API format

The GUI workflow JSON and the API workflow JSON are different formats. The GUI format includes node positions, colors, and UI state. The API format is stripped down to just inputs and connections — which is all the server needs.

To export:

  1. Open ComfyUI in your browser
  2. Go to Settings (gear icon) → enable "Dev Mode Options"
  3. A new button appears in the menu: "Save (API Format)"
  4. Click it — this downloads workflow_api.json

The API format uses numeric string keys ("1", "2", "3") as node IDs. Each node has a class_type and an inputs object. Connections between nodes are expressed as ["source_node_id", output_index] arrays rather than named references.

Here is a minimal SDXL baseline workflow in API format:

{
  "4": {
    "class_type": "CheckpointLoaderSimple",
    "inputs": {
      "ckpt_name": "sd_xl_base_1.0.safetensors"
    }
  },
  "5": {
    "class_type": "EmptyLatentImage",
    "inputs": {
      "batch_size": 1,
      "height": 1024,
      "width": 1024
    }
  },
  "6": {
    "class_type": "CLIPTextEncode",
    "inputs": {
      "clip": ["4", 1],
      "text": "a photorealistic red fox in snow, golden hour lighting"
    }
  },
  "7": {
    "class_type": "CLIPTextEncode",
    "inputs": {
      "clip": ["4", 1],
      "text": "blurry, low quality, watermark, text"
    }
  },
  "3": {
    "class_type": "KSampler",
    "inputs": {
      "cfg": 7.0,
      "denoise": 1.0,
      "latent_image": ["5", 0],
      "model": ["4", 0],
      "negative": ["7", 0],
      "positive": ["6", 0],
      "sampler_name": "euler",
      "scheduler": "normal",
      "seed": 42,
      "steps": 20
    }
  },
  "8": {
    "class_type": "VAEDecode",
    "inputs": {
      "samples": ["3", 0],
      "vae": ["4", 2]
    }
  },
  "9": {
    "class_type": "SaveImage",
    "inputs": {
      "filename_prefix": "api_output",
      "images": ["8", 0]
    }
  }
}

For SDXL specifically, node "4" connects to both CLIP encoders and the KSampler via its three outputs: [0] = model, [1] = CLIP, [2] = VAE. The connection syntax ["4", 1] means "output index 1 of node 4."


Step 3: Queue a prompt from Python

import uuid
import json
import requests

SERVER = "127.0.0.1:8188"
CLIENT_ID = str(uuid.uuid4())

def queue_prompt(workflow: dict) -> str:
    """Submit a workflow. Returns the prompt_id for tracking."""
    payload = {"prompt": workflow, "client_id": CLIENT_ID}
    r = requests.post(f"http://{SERVER}/prompt", json=payload)
    r.raise_for_status()
    return r.json()["prompt_id"]

The client_id is a UUID you generate once per session. It ties your WebSocket connection to your HTTP requests so the server routes status messages back to you. You can skip it for pure HTTP polling, but you need it for WebSocket tracking.

The server responds immediately with {"prompt_id": "<uuid>", "number": <queue_position>}. Generation hasn't started yet — the prompt is in queue.


Step 4: Poll for completion

Two approaches — choose based on your use case:

Approach Latency Complexity Best for
HTTP polling /history/{id} ~1s overhead Low — no extra library Scripts, batch jobs
WebSocket /ws?clientId=... Near-real-time Medium — event loop Apps that show progress

HTTP polling (simpler)

import time

def wait_for_completion(prompt_id: str, poll_secs: float = 1.0) -> dict:
    """Block until the prompt finishes. Returns the history entry."""
    url = f"http://{SERVER}/history/{prompt_id}"
    while True:
        r = requests.get(url)
        r.raise_for_status()
        history = r.json()
        if prompt_id in history:
            return history[prompt_id]
        time.sleep(poll_secs)

The /history/{prompt_id} endpoint returns an empty dict {} while the prompt is queued or running, and the full result object once done. Polling every second adds at most 1 second of latency to your total generation time — acceptable for batch scripts.

WebSocket approach (for real-time apps)

import websocket

def generate_with_ws(workflow: dict) -> str:
    """Queue and wait for completion via WebSocket. Returns prompt_id."""
    ws = websocket.WebSocket()
    ws.connect(f"ws://{SERVER}/ws?clientId={CLIENT_ID}")
    prompt_id = queue_prompt(workflow)

    while True:
        raw = ws.recv()
        if not isinstance(raw, str):
            continue  # binary preview frames — skip
        msg = json.loads(raw)
        if msg["type"] == "executing":
            data = msg["data"]
            if data["node"] is None and data["prompt_id"] == prompt_id:
                break  # null node = generation finished

    ws.close()
    return prompt_id

The executing message fires for each node as it runs. When node is None and the prompt_id matches yours, the entire graph has finished executing.


Step 5: Download the generated images


python
import os

def download_images(history_entry: dict, output_dir: str = "output") -> list[str]:
    """Download all output images from a completed prompt."""
    os.makedirs(output_dir, exist_ok=True)
    saved = []