- Export the workflow in ComfyUI’s API format.
- Package ComfyUI, your custom nodes, and your model weights.
- Write the
fal.Appthat runs the workflow. - Test it with
fal run, then deploy it withfal deploy. - Set the autoscaling parameters.
- Call the endpoint with cURL and Python.
Prerequisites
- A fal account with Serverless access, and the fal CLI installed and logged in:
- 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.
- Open the workflow in ComfyUI and run it once to confirm it works.
- 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).
- Save the file as
workflow_api.jsonnext to your app file.
class_type and inputs:
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 image must include
curl. - The
pydantic,protobuf, andboto3packages 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 insetup() 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:
Step 3: Write the App
Save this ascomfy_app.py. Replace the node IDs in _build_workflow() with the IDs you wrote down in Step 1.
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 /promptqueues the workflow, andGET /history/{prompt_id}reports its outputs when it completes. Image.from_pathuploads each output file to the fal CDN and returns its URL. Passingrequestattaches the metadata the playground uses.
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:
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.
- cURL
- Python
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_concurrencyat 0 unless you need no cold starts. Each runner it keeps up is billed even without traffic. - Size
keep_aliveto the gap between your requests. A longkeep_aliveon sparse traffic is billed idle time. - Keep weights in
/data(Step 2), so new runners do not download them again.
Troubleshooting
- “Model not found” or a missing file in a loader node. The key in
MODELSmust match the file name in the workflow JSON, under the rightmodels/subfolder. - A node type is missing when the workflow runs. Install its custom node package in the Dockerfile (Step 2), then run
fal runagain. - The runner times out during setup. Raise
startup_timeout. The first runner downloads all the weights.
Next Steps
- Deploy a ComfyUI SDXL Turbo App: the complete single-model example, with an architecture diagram.
- Use a Custom Container Image: Dockerfile options and private registries.
- Scale Your Application: all scaling parameters.
- Optimize Cold Starts: reduce the time before the first request runs.