ComfyUI API: Deploy a ComfyUI Workflow as an Endpoint

Turn a ComfyUI workflow into an API on fal Serverless: export it in API format, wrap ComfyUI in a fal.App, keep checkpoints on /data and deploy an autoscaling GPU endpoint.

Jim Cai9 min read
ComfyUI API: Deploy a ComfyUI Workflow as an Endpoint

Export your workflow with Save (API Format), then deploy a fal.App that starts ComfyUI in setup(), fills each request's inputs into the workflow JSON and returns the output from fal's CDN. Keep checkpoints on /data with download_model_weights rather than in the image. Test with fal run, ship with fal deploy, and the app scales to zero when idle, billed per second of runner time. An existing ComfyUI Docker image can deploy unchanged with Direct Server Mode.

A ComfyUI workflow is easy to run on your own GPU, but harder to put behind an API. Other people's code can't click Queue Prompt. To serve it, you need a server that accepts a request, fills the inputs into your graph, runs it and returns a file. That server also has to cope when ten requests arrive at once and when none arrive for an hour.

On fal Serverless, that server is a small Python class. It starts ComfyUI once per GPU runner and exposes your workflow as an endpoint. fal then adds runners as requests arrive and removes them when traffic stops. You pay per second for the time a runner is alive. This guide walks through the approach in fal's ComfyUI deployment example, from exporting the workflow to calling the deployed endpoint.

What you are building

The finished app has four parts:

  1. Your workflow, exported from ComfyUI as JSON.
  2. A fal.App class that starts ComfyUI as a background process in setup(), then waits until it answers.
  3. An endpoint, for example /generate. It takes typed inputs (prompt, size, seed), writes them into the workflow JSON and queues the job on ComfyUI's local HTTP API. When the job finishes, it uploads the output image to fal's CDN.
  4. Model weights on /data, the persistent volume every runner in your account mounts. The first runner downloads the checkpoints, and every later runner reuses them.

Callers never talk to ComfyUI directly. They call your-username/your-app/generate with an API key and get back an image URL. Your app sits behind fal's queue, so it gets the same queueing as the hosted models in the Model APIs catalog.

Step 1: Export the workflow in API format

Build and test the workflow in the ComfyUI web interface first. Then click Save (API Format). This export is different from the normal save: it writes a flat map of node IDs to their inputs. That is the format ComfyUI's /prompt endpoint accepts.

Open the JSON and note the IDs of the nodes your API will change. In the SDXL Turbo workflow from the docs example, these are:

Node IDNodeInput your API sets
6CLIP Text Encode (positive)text ← prompt
7CLIP Text Encode (negative)text ← negative prompt
5Empty Latent Imagewidth, height
13SamplerCustomnoise_seed ← seed
22SDTurboSchedulersteps

Your IDs will be different. Read them from your own export, because the app uses them to fill in each request.

Step 2: Wrap ComfyUI in a fal.App

The app builds a container with ComfyUI installed, then starts it in setup(). setup() runs once when a runner starts, not once per request. So the server and the loaded models stay in memory for every request that runner handles.

python
class ComfyUISDXLTurbo(fal.App, keep_alive=300, max_concurrency=1):
    machine_type = "GPU-A100"
    image = ContainerImage.from_dockerfile_str(DOCKERFILE_STR)

    def setup(self):
        # Download models to persistent /data storage
        self._download_models()
        self._link_models()

        # Start ComfyUI server in background (non-blocking)
        self.process = subprocess.Popen(
            ["python", "main.py", "--listen", self.COMFYUI_HOST, "--port", str(self.COMFYUI_PORT)],
            cwd=str(self.COMFYUI_DIR),
        )
        self._wait_for_server(timeout=120)

        # Load the workflow template that each request fills in
        with open("/app/workflow.json", encoding="utf-8") as f:
            self.workflow_template = json.load(f)

    @fal.endpoint("/generate")
    def generate_image(self, input: ImageRequest, request: Request) -> ImageResponse:
        workflow = self._build_workflow(input)
        prompt_id = self._queue_prompt(workflow)
        history = self._poll_for_completion(prompt_id)
        image_path = self._get_output_image(history)
        image = Image.from_path(image_path, request=request)
        return ImageResponse(image=image, seed=input.seed)

An excerpt from the full example, which also defines the Dockerfile, the request and response models, and the helper methods.

The helpers are short:

  • _build_workflow() deep-copies the workflow template and writes the request's fields into the node IDs from step 1.
  • _queue_prompt() posts the workflow to ComfyUI's POST /prompt and returns the prompt_id.
  • _poll_for_completion() polls GET /history/{prompt_id} until the job is marked complete.
  • Image.from_path() uploads the output file to fal's CDN and returns a URL. Passing the request attaches the metadata the fal Playground needs.

subprocess.Popen starts ComfyUI without blocking, and _wait_for_server() polls ComfyUI's /system_stats until it responds. setup() then loads the workflow template and returns only when ComfyUI is ready, so the runner never takes a request it can't serve.

The request and response classes are Pydantic models. fal generates an OpenAPI schema from them, which powers the Playground form and the endpoint's documented inputs and outputs.

Getting the workflow file into the container

The example uploads the workflow JSON to fal's CDN with File.from_path(..., repository="cdn"). Then the Dockerfile fetches it with ADD, which keeps the workflow out of the app code. The custom container guide also describes COPY and ADD for local files, which is currently experimental.

Step 3: Keep checkpoints on /data, not in the image

Don't bake large checkpoints into the Docker image. Runners pull the image on a cold start, so a bigger image means a slower start. Instead, the example downloads weights in setup() with download_model_weights from the fal toolkit:

  • It skips the download if the file is already there, so later runners reuse it instead of downloading it again.
  • It writes atomically: it downloads to a temporary file and then renames it. Two runners starting at the same moment can't corrupt each other's download.
  • It stores files under /data/.fal/model_weights/.

ComfyUI expects models in its own models/ directories, so the example symlinks each downloaded file into place. If a run fails with "model not found", check these three things: the file is on /data, the symlink points to it, and the workflow uses the same filename.

You can also upload weights to /data ahead of time, from the Files page in the dashboard or with fal files upload. See persistent storage for the size limits of each upload method.

Step 4: Test on a temporary URL, then deploy

bash
pip install fal
fal auth login
fal run comfyui_sdxl_turbo.py

fal run starts the app on a real GPU runner and prints a temporary URL. It runs setup() and your endpoint the same way production will. Import errors, missing custom nodes and model paths therefore fail here, not after you deploy. The temporary URL is public by default, so add --auth private if it shouldn't be callable without a key.

When it works, deploy it with an explicit app name:

bash
fal deploy comfyui_sdxl_turbo.py --app-name comfyui-sdxl-turbo

Without --app-name (or an app_name attribute on the class), fal derives the name from the class name by splitting it at every capital letter. ComfyUISDXLTurbo would become comfy-u-i-s-d-x-l-turbo. The deployed app gets a permanent endpoint ID, your-username/comfyui-sdxl-turbo, and requires an API key by default.

Call it with a fal client:

python
import fal_client

result = fal_client.subscribe(
    "your-username/comfyui-sdxl-turbo/generate",
    arguments={"prompt": "a glass bottle with a galaxy inside", "seed": 42},
)
print(result["image"]["url"])

subscribe submits the request to the queue and waits for the result. For long workflows you can also submit and poll, or pass a webhook URL.

Already have a ComfyUI Docker image?

If ComfyUI already runs in a container you maintain, you don't have to write a fal.App wrapper. Direct Server Mode sends traffic straight to a port in your container. Configure it in pyproject.toml:

[tool.fal.apps.my-server]
auth = "private"
machine_type = "GPU-A100"
exposed_port = 8000
keep_alive = 300

[tool.fal.apps.my-server.image]
dockerfile = "Dockerfile"

The server must listen on 0.0.0.0 on that port. Callers then use your server's own routes under your endpoint ID. Direct Server Mode supports the same scaling parameters as a fal.App. It can also pull a prebuilt image from Docker Hub, Google Artifact Registry, Amazon ECR or Azure Container Registry.

There is one trade-off. The Playground, analytics and error tracking need an OpenAPI spec. A fal.App generates one for you, but in Direct Server Mode your server must serve it at /openapi.json. ComfyUI's native API is shaped around workflow graphs, not typed inputs. If you want a clean, typed API for callers, the fal.App wrapper from step 2 is usually the better fit. See Migrate an external Docker server for both options side by side.

Choosing a GPU

Pick the smallest GPU your models fit on. The example runs on GPU-A100 (40 GB). fal's machine types guide recommends GPU-L40 (48 GB) or GPU-RTXPRO6000 (96 GB) for image generation. The RTX PRO 6000's extra memory gives headroom for high-resolution and batched workloads.

You can also give machine_type a list, for example ["GPU-L40", "GPU-A100"]. fal tries the types in order and uses the next one when the first has no free capacity.

Scaling and cost

The example sets two scaling parameters on the class:

  • keep_alive=300 keeps a runner alive for five minutes after its last request, so a burst of requests doesn't pay a cold start each time. The default is 60 seconds.
  • max_concurrency=1 caps the app at one runner, which is sensible while you test. Raise it to let fal scale out under load.

min_concurrency defaults to 0. With no traffic, runners shut down when keep_alive expires, and the app scales to zero. Billing is per second of runner lifetime by machine type. A runner is billed for its whole lifetime, from setup() through idle time, requests, draining and shutdown. Time spent waiting for hardware or pulling the image is not billed. The serverless inference guide explains how to tune these settings for real traffic.

FAQ

How do I turn a ComfyUI workflow into an API?

Export the workflow with Save (API Format). Then deploy a fal.App that starts the ComfyUI server in setup() and exposes an endpoint. The endpoint fills your inputs into the workflow JSON, queues it on ComfyUI and returns the output. fal deploy gives the app a permanent endpoint ID (your-username/app-name), which you call with the fal client SDKs or the REST API.

Can I deploy my existing ComfyUI Docker image without rewriting it?

Yes. Use Direct Server Mode: set exposed_port in pyproject.toml, point the app at your Dockerfile or registry image, and make the server listen on 0.0.0.0 on that port. Your existing routes work unchanged, with the same scaling parameters as a fal.App.

Where should ComfyUI checkpoints and custom models live?

On the persistent /data volume. The fal toolkit's download_model_weights skips files that already exist and writes atomically, so concurrent runners don't corrupt a download. The docs recommend this over baking weights into the image, because runners pull the image on a cold start and larger images start slower.

Which GPU should I run ComfyUI on?

Pick the smallest GPU your models fit on. The ComfyUI example uses GPU-A100 (40 GB). The machine types guide recommends L40 or RTX PRO 6000 for image generation. You can list several machine types, and fal tries them in order when the first has no capacity.

Do I pay while my ComfyUI endpoint is idle?

Only while a runner is alive. Idle time within keep_alive is billed. With the default min_concurrency of 0, runners shut down when keep_alive expires, and nothing is billed after a runner stops.

Do I need approval to deploy ComfyUI on fal?

Yes. Deploying your own apps on fal Serverless needs access that the fal team approves for each account. Request it on the Serverless Get Started page. Creating a fal account is free, and calling hosted Model APIs needs no approval.

Build with generative media on fal

Hundreds of production-ready image, video, and audio models behind one API.