Published on

ComfyUI API 큐·워크플로우 운영 실전 가이드

Authors

서버에 ComfyUI를 올려두고 prompt API로 Stable Diffusion 이미지를 뽑기 시작하면, 곧바로 운영 문제가 터집니다. 요청이 몰리면 GPU가 한 장인데도 프론트는 계속 눌러대고, 워크플로우 JSON을 조금만 바꿔도 결과가 달라져 재현이 안 되며, 작업이 길어지면 타임아웃·메모리 누수·디스크 고갈 같은 인프라 이슈가 함께 등장합니다.

이 글은 ComfyUI API를 큐 기반으로 안전하게 운영하기 위한 설계 포인트를 정리합니다. 특히 다음 세 가지를 목표로 합니다.

  • 큐를 통해 GPU를 단일 스레드처럼 안정적으로 사용하기
  • 워크플로우를 버전·파라미터·시드까지 포함해 재현 가능하게 만들기
  • 장애가 나도 요청 유실 없이 재시도·격리·관측할 수 있게 만들기

ComfyUI API 기본 흐름: 제출·실행·수집

ComfyUI는 보통 8188 포트에서 HTTP와 WebSocket을 제공합니다. 운영 관점에서 중요한 건 “동기 호출로 결과를 기다리는 방식”보다 비동기 잡 모델로 다루는 것입니다.

일반적인 흐름은 아래와 같습니다.

  1. 클라이언트가 워크플로우(JSON)를 만들어 POST /prompt로 제출
  2. 응답으로 prompt_id를 받음
  3. WebSocket으로 실행 이벤트를 구독하거나, 히스토리 API로 완료 여부를 폴링
  4. 완료 후 outputs에 기록된 파일명을 기준으로 이미지 다운로드

여기서 핵심은 ComfyUI 내부에도 큐가 있지만, 운영 시스템 관점에서는 다음을 분리하는 게 안전합니다.

  • 외부 큐: 사용자 요청을 수용하고 우선순위·쿼터·리트라이를 제어
  • 내부 큐: ComfyUI가 GPU에서 실제로 그래프를 실행하는 대기열

즉, “외부 큐에서 1건씩 꺼내 ComfyUI에 제출”하는 형태가 가장 단순하면서 견고합니다.

큐 운영 전략: 한 GPU, 한 워커(원칙)

ComfyUI 워크플로우는 그래프 실행 중 VRAM을 크게 점유합니다. 여러 요청을 동시에 넣어도 결국 GPU 메모리 경합으로 실패하거나, 느려지거나, OOM으로 죽을 수 있습니다. 운영 원칙은 다음이 좋습니다.

  • GPU 1장 당 워커 1개
  • 워커는 외부 큐에서 잡을 1개씩 가져와 ComfyUI에 제출
  • 워커는 잡 완료까지 상태를 추적하고, 완료 시 결과를 영속화

이 패턴을 쓰면 “동시성”은 GPU 단위로만 제한되고, 나머지는 큐에서 흡수합니다.

외부 큐에 넣을 최소 메타데이터

잡 메시지에는 최소한 아래가 들어가야 합니다.

  • job_id: 외부 시스템의 식별자
  • workflow_version: 워크플로우 템플릿 버전
  • params: 사용자 입력(프롬프트, 네거티브, 시드, 사이즈 등)
  • priority: 유료/무료, 내부 운영 잡 등
  • idempotency_key: 중복 제출 방지 키

이렇게 해야 ComfyUI의 prompt_id가 바뀌어도 외부 시스템에서 잡을 추적할 수 있습니다.

워크플로우 운영: “템플릿 + 파라미터 주입”으로 고정

ComfyUI 워크플로우는 JSON 그래프이며, 노드의 입력값을 바꾸는 방식으로 프롬프트·시드·모델 등을 주입합니다. 운영에서 중요한 건 “사용자 입력을 곧바로 워크플로우 JSON 전체로 받지 말 것”입니다.

  • 워크플로우 JSON 전체를 외부에서 받으면 보안·재현성·호환성이 무너짐
  • 대신 워크플로우는 서버가 관리하는 템플릿으로 고정
  • 사용자 입력은 허용된 필드만 매핑하여 노드 입력에 주입

워크플로우 템플릿 예시(일부)

아래는 설명을 위한 축약 예시입니다. 실제 ComfyUI JSON은 노드가 더 많습니다.

{
  "1": {
    "class_type": "CheckpointLoaderSimple",
    "inputs": { "ckpt_name": "sdxl_base_1.0.safetensors" }
  },
  "2": {
    "class_type": "CLIPTextEncode",
    "inputs": { "text": "a photo of a cat", "clip": ["1", 1] }
  },
  "3": {
    "class_type": "CLIPTextEncode",
    "inputs": { "text": "low quality", "clip": ["1", 1] }
  },
  "4": {
    "class_type": "KSampler",
    "inputs": {
      "seed": 123,
      "steps": 30,
      "cfg": 7,
      "sampler_name": "euler",
      "scheduler": "karras",
      "positive": ["2", 0],
      "negative": ["3", 0],
      "model": ["1", 0]
    }
  }
}

운영에서는 이 템플릿을 workflow_version으로 관리하고, 배포 시점에 변경 이력을 남기는 게 중요합니다.

파라미터 주입 코드(Node.js)

아래는 템플릿을 불러와 특정 노드 입력만 안전하게 바꾸는 예시입니다.

import fs from "node:fs";

export function buildPrompt({
  templatePath,
  prompt,
  negative,
  seed,
  steps,
  cfg,
}) {
  const wf = JSON.parse(fs.readFileSync(templatePath, "utf-8"));

  // 템플릿에서 정해둔 노드 ID에만 주입
  wf["2"].inputs.text = String(prompt ?? "");
  wf["3"].inputs.text = String(negative ?? "");

  // seed가 없으면 서버에서 생성해도 됨
  wf["4"].inputs.seed = Number.isFinite(seed) ? seed : Math.floor(Math.random() * 2 ** 31);
  wf["4"].inputs.steps = Math.max(1, Math.min(steps ?? 30, 80));
  wf["4"].inputs.cfg = Math.max(1, Math.min(cfg ?? 7, 20));

  return wf;
}

이 방식의 장점은 다음과 같습니다.

  • 워크플로우 구조는 고정되어 재현성이 좋아짐
  • 허용된 입력만 바뀌므로 보안이 좋아짐
  • 템플릿을 바꾸면 workflow_version을 올려서 호환성을 관리할 수 있음

ComfyUI에 제출: POST /prompt와 idempotency

ComfyUI에 프롬프트를 제출하는 워커 코드는 단순해야 합니다. 중요한 건 “네트워크 오류나 워커 재시작”이 있어도 중복 생성이 되지 않도록 외부 시스템에서 멱등성을 보장하는 것입니다.

  • 외부 DB에 idempotency_key를 유니크로 저장
  • 동일 키로 요청이 오면 기존 job_id의 상태를 반환

ComfyUI 자체는 멱등 키를 기본 제공하지 않는 경우가 많으므로, 운영 레이어에서 막는 편이 안전합니다.

export async function submitToComfyUI({ baseUrl, clientId, promptGraph }) {
  const res = await fetch(`${baseUrl}/prompt`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ prompt: promptGraph, client_id: clientId })
  });

  if (!res.ok) {
    const text = await res.text();
    throw new Error(`ComfyUI submit failed: ${res.status} ${text}`);
  }

  const data = await res.json();
  // 보통 data.prompt_id 형태
  return data;
}

상태 추적: WebSocket 이벤트 기반으로 설계

폴링만으로도 동작은 하지만, 운영에서는 WebSocket 이벤트가 훨씬 유리합니다.

  • 진행률, 현재 노드, 완료 이벤트를 받아 타임아웃·취소·알림을 구현하기 쉬움
  • 잡이 길어질수록 폴링은 트래픽과 비용이 증가

다만 WebSocket은 끊길 수 있으므로 “이벤트 기반 + 최종 상태는 히스토리로 확인” 패턴이 안전합니다.

WebSocket 구독 예시

아래 예시는 개념 코드입니다. 실제 이벤트 필드명은 ComfyUI 버전에 따라 다를 수 있어, 운영 전 반드시 로컬에서 이벤트 페이로드를 캡처해 스키마를 고정하세요.

import WebSocket from "ws";

export function watchComfyUI({ baseUrl, clientId, onEvent }) {
  const wsUrl = baseUrl.replace("http://", "ws://").replace("https://", "wss://");
  const ws = new WebSocket(`${wsUrl}/ws?clientId=${encodeURIComponent(clientId)}`);

  ws.on("message", (buf) => {
    const msg = JSON.parse(buf.toString("utf-8"));
    onEvent(msg);
  });

  return ws;
}

운영 팁:

  • 워커는 prompt_id 단위로 이벤트를 필터링
  • 특정 시간 동안 executing 이벤트가 없으면 “정지”로 판단하고 재시작/재시도
  • 최종 완료는 히스토리 API로 확인해 이벤트 유실을 보완

결과 수집: 파일명 의존을 줄이고 아티팩트 스토리지로

ComfyUI는 기본적으로 로컬 디스크에 이미지를 저장하고 파일명을 히스토리에 남깁니다. 하지만 운영에서 로컬 디스크는 다음 문제가 있습니다.

  • 컨테이너 재시작 시 유실
  • 노드 장애 시 유실
  • 디스크 100퍼센트로 서비스 전체 장애

따라서 결과 이미지는 가능한 빨리 **외부 스토리지(S3 호환, NFS, 오브젝트 스토리지)**로 옮기고, 외부 DB에는 그 URL만 저장하는 구조가 좋습니다.

디스크가 꽉 차는 문제는 생각보다 자주 발생합니다. 이미지·중간 산출물·캐시가 누적되면 du도 느려져 원인 파악이 어려워지는데, 이 상황은 아래 글의 접근법이 그대로 도움이 됩니다.

장애 대응 1: OOM, VRAM 부족, OOMKilled

Stable Diffusion은 워크플로우에 따라 메모리 사용량이 크게 변합니다. 특히 SDXL, 고해상도, 업스케일, ControlNet 다중 적용은 쉽게 한계를 넘습니다.

운영에서 자주 보는 패턴:

  • 컨테이너가 OOMKilled
  • 특정 입력(예: 2048x2048)에서만 반복적으로 죽음
  • 재시도하면 또 죽어서 큐가 막힘

대응 전략:

  • 워크플로우 템플릿에서 허용 해상도·배치 크기 상한을 강제
  • 워커가 잡 실행 전 “예상 비용”으로 라우팅(고비용 잡은 전용 GPU로)
  • 쿠버네티스라면 cgroup v2 기준으로 메모리/스왑/페이지 캐시를 함께 관측

OOMKilled 루프를 진단하는 방법은 아래 글이 매우 실전적입니다.

장애 대응 2: 워커/서비스 재시작과 잡 유실 방지

ComfyUI 프로세스가 죽거나, 노드가 재부팅되거나, 워커가 업데이트로 재시작될 수 있습니다. 이때 중요한 건 “어느 잡이 실행 중이었는지”를 외부에서 알고 있어야 한다는 점입니다.

추천 상태 모델:

  • queued: 외부 큐에 있음
  • submitted: ComfyUI에 제출했고 prompt_id를 받음
  • running: 이벤트로 실행 시작 확인
  • succeeded: 결과 업로드 완료
  • failed_retryable: 재시도 가능
  • failed_terminal: 입력 문제 등으로 재시도 불가

그리고 워커 재시작 시에는 다음을 수행합니다.

  • submitted 또는 running 상태 잡을 조회
  • 해당 prompt_id가 ComfyUI 히스토리에 남아 있는지 확인
  • 없으면 재제출(단, 멱등키로 중복 생성 방지)

리눅스 환경에서 서비스가 반복 재시작될 때 원인을 추적하는 방법도 함께 알아두면 좋습니다.

큐 정책: 우선순위, 쿼터, 취소, 데드레터

운영이 커지면 “큐를 넣는 것”만으로는 부족합니다. 최소한 아래 기능이 필요합니다.

우선순위

  • 유료 사용자, 내부 배치, 관리자 요청을 우선 처리
  • 구현은 큐를 여러 개로 나누거나, 단일 큐에서 priority 필드를 사용

쿼터

  • 사용자 단위 초당 제출량 제한
  • 사용자 단위 동시 실행 제한(실제로는 GPU 단위로 제한되더라도, 대기열 폭주를 막아야 함)

취소

  • ComfyUI 쪽 취소 API가 있으면 연동
  • 없거나 불안정하면 워커가 “다음 잡을 안 꺼내는 것”만으로는 부족하므로, 실행 중 잡을 중단하기 위해 프로세스 재시작 같은 강한 조치가 필요할 수 있음

데드레터 큐

  • 동일 잡이 N회 실패하면 데드레터로 보내고, 운영자가 원인을 분석
  • 입력 검증 실패는 즉시 터미널 실패로 분류

워크플로우 버전관리: 결과 재현을 위한 체크리스트

“같은 프롬프트인데 결과가 달라요”는 운영에서 가장 비용이 큰 이슈 중 하나입니다. 재현성을 위해 아래를 로그로 남기세요.

  • workflow_version과 템플릿 파일 해시
  • 모델 파일명과 해시(체크포인트, LoRA, VAE)
  • seed, sampler, scheduler, steps, cfg
  • 이미지 크기, 배치 크기
  • ComfyUI 버전, 커스텀 노드 목록
  • GPU 종류와 드라이버, CUDA 버전

특히 커스텀 노드가 섞이면 같은 워크플로우라도 결과가 바뀔 수 있으니, 노드 목록을 고정하고 배포 단위로 묶는 게 좋습니다.

쿠버네티스 운영 포인트: 스케일링과 GPU 워커 분리

ComfyUI API 운영을 쿠버네티스로 옮기면 장점이 많지만, 함정도 있습니다.

  • HPA는 GPU 사용률을 기본으로 보지 못하는 경우가 많음
  • 이미지 생성은 CPU보다 GPU가 병목이므로, CPU 지표 기반 스케일링은 잘못된 결정을 내릴 수 있음

추천 구조:

  • API 서버(요청 수신, 인증, 큐 적재): CPU 오토스케일
  • 워커(큐 소비, ComfyUI 제출, 결과 수집): GPU 노드에 고정 스케일 또는 커스텀 메트릭 기반 스케일

HPA가 기대대로 늘지 않을 때 메트릭 파이프라인부터 점검하는 방법은 아래 글이 참고됩니다.

운영에서 자주 쓰는 “안전장치” 모음

마지막으로, 실제 장애를 줄여주는 안전장치를 체크리스트로 정리합니다.

  • 입력 검증: 해상도, steps, batch, ControlNet 개수 상한
  • 타임아웃: 잡 최대 실행 시간, 이벤트 정지 시간 감지
  • 리트라이: 네트워크 오류만 재시도, 입력 오류는 즉시 실패
  • 격리: 고비용 워크플로우는 전용 큐·전용 GPU로 분리
  • 스토리지: 로컬 산출물 TTL 정리, 결과는 오브젝트 스토리지로 즉시 업로드
  • 관측: 잡 단위 로그에 job_id, prompt_id, workflow_version 포함
  • 배포: 워크플로우 템플릿 변경은 버전 업, 롤백 가능하게

마무리: “ComfyUI는 실행기, 운영은 큐가 한다”

ComfyUI를 API로 붙여 운영할 때 가장 흔한 실패는 “ComfyUI 내부 큐에만 의존”하는 것입니다. 외부 큐와 워커를 두고, 워크플로우를 템플릿화하며, 결과를 영속화하면 다음이 가능해집니다.

  • 트래픽이 몰려도 GPU는 안정적으로 1건씩 처리
  • 워크플로우 변경이 있어도 버전으로 재현 가능
  • 프로세스가 죽어도 잡 유실 없이 복구

결국 운영의 중심은 Stable Diffusion 모델이 아니라 **잡의 생명주기(큐·상태·아티팩트)**입니다. 이 관점으로 설계를 잡으면 ComfyUI는 훨씬 다루기 쉬운 “그래프 실행 엔진”이 됩니다.