Skip to main content
This guide turns a ComfyUI workflow that works on your machine into a private, autoscaling HTTP API on fal Serverless. The app runs the ComfyUI server inside a custom container, queues your workflow on it for each request, and returns the output images through fal’s CDN. You will:
  1. Export the workflow in ComfyUI’s API format.
  2. Package ComfyUI, your custom nodes, and your model weights.
  3. Write the fal.App that runs the workflow.
  4. Test it with fal run, then deploy it with fal deploy.
  5. Set the autoscaling parameters.
  6. Call the endpoint with cURL and Python.
The ComfyUI SDXL Turbo example is a complete, single-model version of this pattern. This guide covers the parts that change when you bring your own workflow: custom nodes, several models, and scaling.

Prerequisites

  • A ComfyUI workflow that runs correctly in your local ComfyUI.
  • The download URL of every model file the workflow loads (checkpoints, LoRAs, VAEs, ControlNets, and so on).

Step 1: Export the Workflow API JSON

ComfyUI saves workflows in two formats. The normal save format describes the canvas layout. The API format describes only the nodes and their inputs, keyed by node ID. The ComfyUI server’s /prompt endpoint accepts only the API format.
  1. Open the workflow in ComfyUI and run it once to confirm it works.
  2. Export it in API format. In the current ComfyUI interface this is File > Export (API) in the top menu. In the legacy menu, first turn on Enable dev mode options (API save, etc.) in the settings, then click Save (API Format).
  3. Save the file as workflow_api.json next to your app file.
The file maps each node ID to its class_type and inputs:
Write down the node IDs of the inputs you want to expose in your API, such as the positive prompt text and the sampler seed. You set them in Step 3. To see every node type the workflow uses, run:
Any node type that is not part of core ComfyUI comes from a custom node package. You install those packages in Step 2.

Step 2: Package Custom Nodes and Models

Your project needs two files:

ComfyUI and custom nodes go in the container image

Install ComfyUI and each custom node package in the Dockerfile. Pin each repository to a commit or tag, so that a rebuild cannot pick up an incompatible change:
The fal runtime has two requirements for the image:
  • The image must include curl.
  • The pydantic, protobuf, and boto3 packages must be compatible with the fal SDK. Install them last, as above, to meet this requirement. See fal platform requirements.

Model weights go in persistent storage

Do not bake large model files into the image. A larger image takes longer to pull on every cold start. Download the weights in setup() with download_model_weights. It saves each file under /data, the persistent storage that all your runners share, and skips the download when the file is already there. Only the first runner pays the download time. List each model by its path under ComfyUI’s models/ directory, so the workflow’s loader nodes find it under the same file name:
For gated or private Hugging Face repositories, store your token as a secret. Secrets reach your runners as environment variables:

Step 3: Write the App

Save this as comfy_app.py. Replace the node IDs in _build_workflow() with the IDs you wrote down in Step 1.
How it works:
  • setup() runs once per runner. It downloads or reuses the weights, starts ComfyUI, and loads the workflow template. Requests reuse the running ComfyUI server, so models stay in GPU memory between requests.
  • Each request copies the template and sets only the inputs your API exposes. Every other node keeps the value from your export.
  • ComfyUI’s HTTP API does the work: POST /prompt queues the workflow, and GET /history/{prompt_id} reports its outputs when it completes.
  • Image.from_path uploads each output file to the fal CDN and returns its URL. Passing request attaches the metadata the playground uses.
Choose machine_type by the VRAM your workflow needs. See machine types. If the first model download takes longer than the default setup limit, raise startup_timeout. The Dockerfile in Step 2 fetches the workflow file with ADD from its CDN URL. You can instead put COPY workflow_api.json /app/workflow_api.json in the Dockerfile. fal uploads the local files that COPY names to the build context when you deploy. This is an experimental feature. Container apps do not support app_files.

Step 4: Test, Then Deploy

Run the app on a temporary runner first. fal run builds the image, runs setup(), and gives you a temporary URL, so missing nodes and wrong model paths show up before you deploy:
When the test requests succeed, deploy it:
The app is now available at the endpoint ID your-username/comfy-workflow. Apps are private by default, so callers need a fal API key. Each deploy creates a new revision that you can roll back to. See Deploy to Production for authentication modes and environments.

Step 5: Set Autoscaling

fal starts runners when requests arrive and stops them when they are idle. These scaling parameters control the trade-off between latency and cost: Set them as class attributes, as in Step 3, or change them on a live app without a redeploy:
A redeploy keeps the scale settings of the previous deployment. To apply the values in your code instead, deploy with --reset-scale:

Step 6: Call the Endpoint

The workflow runs at the /generate path, so append /generate to the endpoint ID.
Submit to the queue. This is the right default for workflows that take more than a few seconds:
The response contains a request_id, a status_url, and a response_url. Poll the status_url until the request completes, then fetch the result from the response_url, both with the same Authorization header. See Queue.For a short workflow, you can call the endpoint synchronously and wait for the result:

What Billing Looks Like

Serverless apps are billed for the time their runners are alive, per second, at the rate of the machine type. A ComfyUI workflow is not billed per image. See Serverless pricing for the full rules and fal.ai/pricing for the per-second rate of each machine type. Requests that fail with a server error (HTTP 500 or higher) are not charged. Runners with more than one GPU are billed for each GPU. To control cost:
  • Keep min_concurrency at 0 unless you need no cold starts. Each runner it keeps up is billed even without traffic.
  • Size keep_alive to the gap between your requests. A long keep_alive on sparse traffic is billed idle time.
  • Keep weights in /data (Step 2), so new runners do not download them again.
The App Analytics page shows the cost, request count, and runner use of each app. For more ways to cut cost, see Optimizing Costs.

Troubleshooting

  • “Model not found” or a missing file in a loader node. The key in MODELS must match the file name in the workflow JSON, under the right models/ subfolder.
  • A node type is missing when the workflow runs. Install its custom node package in the Dockerfile (Step 2), then run fal run again.
  • The runner times out during setup. Raise startup_timeout. The first runner downloads all the weights.

Next Steps