Author: Junjie

  • ComfyUI –fast Flag Explained: The Untested Speed Toggles and When They Cost You Quality

    If you spend enough time reading ComfyUI launch-command discussions, you will eventually see someone paste a one-word "optimization" and claim it made their generations faster: --fast. It sounds exactly like a free win. But ComfyUI’s own source code describes --fast very differently from the way most tutorials gloss over it. The flag bundles a set of untested performance features, several of which can change your output or crash a workflow outright. In this guide I break down what --fast actually turns on, which of those toggles are genuinely safe to use individually, and how to reason about the speed-versus-quality and speed-versus-stability trade-off before you flip everything on.

    This is a documentation analysis built directly from the ComfyUI source (comfy/cli_args.py) and the official startup-flags reference, not a synthetic benchmark. Where I make a claim about what a flag does, I point to the exact help text in the code. I do not fabricate FPS numbers or "2x faster" results that I have not measured.

    What –fast Actually Is (Not a Magic Speed Switch)

    The full help text for the flag in cli_args.py reads:

    --fast [FAST ...] — Enable some untested and potentially quality deteriorating optimizations. This is used to test new features so using it might crash your ComfyUI.

    Three phrases in that sentence matter and are almost always left out of the "just add --fast" advice:

    • Untested. These are not the stable, default-on optimizations ComfyUI ships for Nvidia cards (like async weight offloading, which is on by default). They are experimental paths.
    • Potentially quality deteriorating. Some features trade numerical precision or skip work in ways that can alter pixels, not just wall-clock time.
    • Might crash. The flag is explicitly documented as a way to test new features, not as a recommended production setting.

    The argument parser handles --fast in three ways. If you pass --fast with no value at all, it enables every feature in the internal PerformanceFeature set. If you pass a list (for example --fast async_cuda_graphs), only those features are enabled. This matters a lot: the difference between "give me one specific experimental toggle" and "turn on every experimental toggle at once" is the difference between a measured, reversible experiment and a mystery box that breaks things.

    Understanding what --fast wraps is also useful because many of the individual features it enables are themselves available as standalone flags. You often want the standalone version, not the bundle. Let’s map the stable flags first, because they are how you get speed without accepting the "untested" label.

    The Safe-by-Default Optimizations You Should Understand First

    Before touching --fast, know which performance features are already on by default or available as stable, documented flags. Pulling the exact help text from cli_args.py:

    --async-offload [NUM_STREAMS]  Use async weight offloading. An optional argument
                                    controls the amount of offload streams. Default is 2.
                                    Enabled by default on Nvidia.
    --disable-async-offload        Disable async weight offloading.
    --reserve-vram GB              Set the amount of vram in GB you want to reserve for
                                    use by your OS/other software.
    --vram-headroom GB             Set the amount of vram in GB for DynamicVRAM to maintain
                                    as extra headroom above default.
    --fast-disk                    Force disk-backed dynamic loading and offload over
                                    unpinned RAM. Can be faster for users with fast NVME disks.
    --disable-smart-memory         Force ComfyUI to aggressively offload to regular RAM
                                    instead of keeping models in VRAM when it can.
    

    The practical takeaway: on an Nvidia GPU you are already getting async weight offloading with two streams by default. That is your normal baseline. The flags above are how you fine-tune memory pressure (“keep some VRAM free for my browser” via --reserve-vram, or “leave a safety buffer” via --vram-headroom) or choose a different offload target (--fast-disk if you have a fast NVMe and want to spill to disk instead of clamping). None of these are “untested.”

    There is an important interaction worth flagging: --fast-disk and the broader caching flags (--cache-ram, --cache-lru, --cache-none) govern where offloaded weights and intermediate results live. If you are chasing a memory problem, changing the cache/offload target is a far more predictable lever than --fast. We have covered the offload-versus-leak diagnosis in detail in our ComfyUI system-RAM OOM guide, and the launch-flag family is mapped in the –lowvram vs –novram vs –normalvram explainer.

    What the Individual Performance Features Do (and Why They’re Risky)

    The reason --fast carries a "quality deteriorating" warning is that it can flip experimental execution paths. Two categories dominate in practice, and both are visible in the source as individual disable flags:

    --disable-cuda-graphs          Disable CUDA graphs.
    --disable-comfy-compiler       Disable the Comfy model compiler, including its CUDA
                                    graph subfeature.
    --assert-graph-breaks          Fail on Comfy model compiler graph breaks.
    --force-non-blocking           Force ComfyUI to use non-blocking operations for all
                                    applicable tensors.
    

    CUDA graphs and the Comfy model compiler are the two big “capture the work once, replay it faster” mechanisms. When they work, they cut kernel-launch overhead and can meaningfully speed up repeated sampler steps. When they don’t work — for example when a custom node introduces a dynamic control-flow branch that breaks graph capture — you get either a fallback (slower) or an outright failure. That is exactly the kind of thing the --assert-graph-breaks flag is there to surface during development: it makes a silent performance fallback into a loud error so developers know a graph broke.

    The honest recommendation here is the reverse of the “turn it all on” instinct. If you suspect a specific workflow is slow, isolate one toggle at a time:

    1. Run your normal workflow and note the generation time.
    2. Enable a single experimental feature (e.g. --fast <feature>) and re-run the same seed, same prompt.
    3. Compare the output image hash. If pixels changed and you didn’t expect them to, that feature altered numerics — treat it as a quality trade, not a free speedup.
    4. If the workflow errors, capture the traceback before touching anything else. A broken graph is often a specific node’s fault, not a reason to disable optimization globally.

    This isolate-then-verify loop is the only way to know whether an experimental toggle actually helps your workflow. It is also why blanket advice like “just add --fast” is unreliable: the features that help one node graph may silently degrade another.

    The Memory Flags That Change Behavior vs. the Ones That Don’t

    A recurring source of confusion is the difference between flags that change where weights live and flags that change how work is computed. They get lumped together under “performance flags,” but the failure modes are completely different.

    The VRAM group is mutually exclusive — you pick one:

    --gpu-only                  Store and run everything (text encoders/CLIP models, etc.)
                                 on the GPU.
    --highvram                  Keep models in GPU memory after use instead of unloading.
    --lowvram                   Run text encoders on CPU (no-op if Dynamic VRAM is active).
    --novram                    When lowvram isn't enough.
    --cpu                       Run everything on the CPU (slow).
    

    These change where weights live, not how the math is done. The output should be byte-identical regardless of which you choose — only speed and memory differ. If switching among --highvram/--lowvram/--gpu-only changes your image, that is a bug worth reporting, not a normal trade-off. By contrast, a flag like --fp16-vae (whose help text literally warns “might cause black images”) changes the numerical precision of an actual computation, so different output is expected.

    Knowing this distinction saves you hours. If your image changes and you only touched memory flags, stop looking at your prompt and start looking for a custom node that pads or slices tensors differently under offload. If your image changes and you enabled an experimental compute feature, that change is the feature doing what its warning said it might.

    One more interaction worth understanding: --lowvram is explicitly a no-op when Dynamic VRAM is enabled (which it is by default on Nvidia unless you pass --highvram, --gpu-only, --novram, or --cpu). People frequently “add --lowvram” to fix an OOM, see no change, and conclude the flag is broken. It isn’t — Dynamic VRAM already applies a more adaptive policy, so the legacy flag is simply inert. If you need to force the old behavior, --disable-dynamic-vram is the switch, and we walk through exactly when that is the right call in the Dynamic VRAM default-OOM guide.

    How to Actually Verify a Performance Flag Helps You

    The core skill here is not memorizing flags — it is building a tiny verification habit so you stop guessing. Here is a concrete, reproducible method that requires no special tooling:

    1. Pin the inputs. Fix the seed, the prompt, the sampler, steps, and CFG. A “before/after” comparison is meaningless if any of these drift.
    2. Record a baseline. Note the wall-clock time and the output filename for the default launch.
    3. Change exactly one thing. One flag. One feature. Nothing else.
    4. Compare time and pixels. Time is obvious. For pixels, hash the two PNGs (or open both and zoom in). If they differ and you only changed a memory flag, investigate; if they differ after an experimental compute toggle, that toggle changed numerics.
    5. Write it down. A one-line note (“--fast <X>: +18% faster, output identical”) beats re-running the whole experiment next month.

    The reason this discipline matters specifically for --fast is its “untested” label. The features inside may change from release to release. A toggle that was safe and helpful in one version can regress in the next, because by definition experimental features are still settling. Your verification note from three months ago may no longer hold — but the habit of verifying will always be correct.

    The official reference to lean on while you experiment is the ComfyUI startup-flags documentation, which mirrors cli_args.py and is the canonical place to check current help text. The changelog is worth a skim too, since new performance features tend to be announced there before most tutorials catch up.

    Conclusion: Treat –fast as a Test Bench, Not a Setting

    The single most useful mental model is this: --fast is a test bench, not a setting. It exists so ComfyUI developers — and curious power users — can enable experimental features incrementally and report what breaks. It is not a “recommended for everyone” launch mode, and its own help text tells you as much in plain language.

    If you take one thing away, take this decision order:

    1. Is async offloading already on for your card? (It is, by default, on Nvidia.) If so, you already have the main free speedup.
    2. Are you memory-limited rather than compute-limited? Fix memory with --reserve-vram, --vram-headroom, or a cache/offload flag — not an experimental compute toggle.
    3. Only if a stable workflow is still slow should you reach for individual --fast features, one at a time, with pinned inputs and a before/after comparison.

    Speed in ComfyUI is almost always about understanding where your weights live and where your work happens — not about flipping an unknown bundle of untested toggles. Use --fast the way it was intended: as a surgical tool for isolated experiments, with your eyes open to the possibility that it changes your output or crashes your run.

    For further reading on the memory side of the same equation, see our attention-backends explainer (SDPA vs SageAttention vs xFormers) and the --vram-headroom vs --reserve-vram guide.

  • ComfyUI Eating System RAM and OOMing: Dynamic VRAM and the PyTorch 2.8 Fix

    Over the last few weeks, a specific class of ComfyUI problem has exploded across Reddit and GitHub: workflows that ran fine a month or two ago now silently balloon system RAM, spill into the page file, and die with an out-of-memory error. The user is on 32GB of RAM and a 16GB GPU, the model is one they have run a hundred times, and yet a 9GB model suddenly consumes 30GB+ of RAM plus another 28GB of page file. The culprit is not a single bug but a collision of two independent changes landing around the same time: the new Dynamic VRAM memory allocator that rolled out in ComfyUI, and a memory regression reported against PyTorch 2.8. This article explains what each of them does, how to diagnose which one is biting you, and the concrete fixes that are actually working for people.

    The Symptom: RAM Spikes on Workflows That Used to Fit

    The canonical report comes from a Reddit user who had 32GB of RAM and 16GB of VRAM. After a round of ComfyUI updates (pulled to get Qwen image-edit support), their Wan workflows that had worked two months prior began crashing with OOM immediately. Qwen image edits worked once and then OOM’d on the next run, forcing a full restart between each image. A 9GB model running alongside an fp8 T5 XXL was eating 30GB+ of RAM plus 28GB of page file — more than a much larger Flux model consumed under Forge. The user’s resolution, documented in their update, was to downgrade to PyTorch 2.7.1 while keeping the latest ComfyUI, after which the RAM issue disappeared entirely.

    The key diagnostic clue in almost every report is the same: the RAM usage is out of proportion to the model size, and it grows across runs rather than dropping back after a generation completes. If your task manager shows committed memory climbing toward your physical RAM limit and then the page file spiking right before the crash, you are almost certainly looking at one of the two causes described below — not a “your GPU is too small” problem.

    What Dynamic VRAM Actually Changes About Memory

    In late 2026, ComfyUI shipped a new memory system called Dynamic VRAM, built on an internal project the team calls the AI Model Dynamic Offloader (aimdo). It is a custom PyTorch VRAM allocator designed to do on-demand offloading of model weights instead of predicting memory needs up front. The official Comfy blog announcement is the best single reference for the internal mechanics, and it is worth reading before you touch any flags.

    Three things changed that matter for the RAM-OOM problem:

    • A new safetensors loader. Files are now opened and mapped to uncommitted, file-backed memory rather than deep-copied into the PyTorch model. The weights are assigned by pointer to uncommitted memory. This is why model loader nodes complete almost instantly in Dynamic VRAM mode.
    • Models are no longer unloaded from VRAM back into RAM. Previously, unloading a model to free VRAM meant writing it back to system RAM — and in multi-model workflows this is exactly what caused RAM exhaustion and page file churn. Under Dynamic VRAM, the VRAM is simply freed and the model returns to the uncommitted load state.
    • Weights stay cached in RAM if you have plenty of it. The official discussion announcing Dynamic VRAM spells this out explicitly: Windows Task Manager may not show lower RAM usage, because cached weights are deliberately kept in RAM to speed things up. The critical difference from before is that these cached weights should never end up in the page file and should be unloaded to make room when other applications need the memory.

    So if you are on a recent ComfyUI build and your RAM appears high but your system is otherwise stable, part of that “high” usage may simply be the new caching behavior working as intended. The page-file spike is the real red flag — that is the behavior Dynamic VRAM was supposed to eliminate, and if you still see it, something else is wrong.

    The PyTorch 2.8 Regression Is a Separate, Identifiable Problem

    Running in parallel with the Dynamic VRAM rollout is a memory regression tracked against PyTorch 2.8. GitHub issue #9259 collects reports of OOM problems that appear specifically after installing the stable PyTorch 2.8 build, with users confirming that the same workflow is fine on 2.7.x and broken on 2.8. The most frequently cited fix in that thread and in the Reddit reports linked above is the same: pin PyTorch back to 2.7.1 while keeping ComfyUI current.

    Disentangling “Dynamic VRAM is misbehaving” from “PyTorch 2.8 is leaking” matters, because the fixes are different. If your RAM problem started immediately after a PyTorch version bump rather than a ComfyUI update, the PyTorch downgrade is the more targeted move. If it started after a ComfyUI update on an already-current PyTorch, the Dynamic VRAM flags are your first stop.

    How to Diagnose and Fix the RAM OOM

    Work through these in order. Each step is cheap and reversible.

    Step 1: Confirm it is a page-file / committed-memory crash

    Open your OS memory monitor (Task Manager on Windows, free -h or htop on Linux) and watch a known-bad workflow run. Note whether committed memory or the page file climbs toward the physical limit right before the crash. A model using RAM out of proportion to its on-disk size, combined with page-file growth, points at unloaded/cached-model memory rather than a genuinely undersized GPU.

    Step 2: Try disabling Dynamic VRAM

    Launch ComfyUI with the --disable-dynamic-vram flag. This is the officially documented workaround for several known problems — notably TensorRT custom nodes such as ComfyUI_TensorRT and ComfyUI-Rife-Tensorrt crash under the new allocator, and some users on high-RAM servers (64GB+) report roughly a 2x slowdown because weight caching that previously worked is no longer effective. If your RAM OOM disappears with the flag, you have isolated the cause to Dynamic VRAM.

    Step 3: If the regression tracks PyTorch, downgrade to 2.7.1

    If the problem appeared after a PyTorch 2.8 install and is unaffected by the Dynamic VRAM flag, pin PyTorch back. On a portable install the relevant command is a version-pinned pip install (pip install torch==2.7.1 — verify your CUDA build string against the official PyTorch instructions) rather than an unpinned upgrade. The Reddit report and issue #9259 both confirm this resolves the RAM spike for affected users.

    Step 4: Verify with a controlled re-run

    After each change, run the exact same workflow that crashed and compare the total workflow execution time (not it/s) and peak committed memory. The Comfy team explicitly asks users reporting Dynamic VRAM performance problems to compare total execution time, because the per-iteration rate can look worse while the end-to-end run is unchanged or better.

    Update First, and Don’t Chase Two Ghosts at Once

    Before any of the flag surgery above, make sure you are on the current ComfyUI build. Dynamic VRAM was, at the time it was first announced, only enabled by default in the latest git version, and several issues discussed in the rollout thread were fixed in subsequent releases. The team has been explicit that they want detailed bug reports — full logs, workflow, hardware, and OS — for any remaining issues. If you are running an older build, you may be debugging behavior that has already been patched.

    It is also easy to confound the two causes. A user who updates ComfyUI (pulling in both Dynamic VRAM and a newer PyTorch at once) cannot tell which change broke them without isolating one variable. Change one thing at a time — flag first, then version pin — and record the peak memory after each step. That discipline is the difference between a 20-minute fix and a week of flailing.

    What This Means for Local AI Hardware Decisions

    The Dynamic VRAM feature is genuinely good news for people on memory-constrained hardware — the official framing is that it makes it possible to run models larger than your RAM without paging, and to avoid the “VRAM OOM because weights were not offloaded enough” failure mode. If you have been sizing a build around physical RAM as the hard ceiling for model size, the allocator’s on-demand offloading changes that calculus. For a deeper look at squeezing large models onto limited VRAM, see our earlier writeup on running Wan2.2 Animate 14B on 16GB VRAM with BlockSwap.

    That said, the rollout has real rough edges. The slowdown reports on high-RAM machines and the TensorRT breakage are reminders that a new allocator interacts with every custom node differently. If you run a production workflow that depends on TensorRT or on a large in-RAM weight cache for speed, treat Dynamic VRAM as an opt-in during this transition window rather than trusting the default silently — and keep the --disable-dynamic-vram escape hatch in your launch script until you have measured your own numbers.

    Conclusion

    If ComfyUI has started eating your system RAM and OOMing on workflows that used to fit, you are almost certainly hitting one of two things: the transition to the Dynamic VRAM allocator (whose new caching can look like a leak but whose page-file behavior is what really matters) or the PyTorch 2.8 memory regression. Confirm which one by watching committed memory and page file, then apply the targeted fix — --disable-dynamic-vram for the allocator, a pin back to PyTorch 2.7.1 for the regression. Update to the latest build first, keep your launch flags in version control, and change one variable at a time. None of this requires a bigger GPU.

    References

  • ComfyUI API: How to Reliably Cancel, Interrupt, and Clear Jobs Queued From Scripts

    Once you take ComfyUI out of the browser and drive it from a script, a simple job that you expected to take thirty seconds can turn into a queue full of runaway video renders. The HTTP API exposes five different ways to stop work, and they operate on different parts of the system. Calling the wrong one does nothing visible, or worse, interrupts a job you actually wanted to keep. This guide walks through each endpoint, what it does to your queue, and the race condition that made job cancellation buggy until recently.

    The Three Jobs States Every Endpoint Hinges On

    Before choosing an endpoint, you need to know which of three states a prompt is in. ComfyUI tracks a prompt from the moment you submit it until it lands in history, and each state is controlled by a different route.

    • Running — the prompt is currently executing on the GPU. Exactly one prompt can be running at a time in the default single-GPU server.
    • Pending — the prompt is sitting in the queue, waiting for its turn after the running prompt finishes.
    • Finished — the prompt has completed (or errored) and moved into history.

    This distinction matters because the endpoint that stops a running prompt is completely different from the endpoint that removes a pending one. A prompt does not magically move between these buckets when you call the wrong route — it just keeps going.

    You can see a snapshot of the first two states at any time with GET /queue. The response body has two lists: queue_running (the current prompt) and queue_pending (everything waiting). Each entry carries the prompt’s UUID, which is the handle you’ll use for every cancellation call below.

    /interrupt Stops Only What Is Running

    POST /interrupt is the bluntest tool and the one most people reach for first. In its simplest form, it stops whatever prompt is currently executing:

    import requests
    
    requests.post("http://127.0.0.1:8188/interrupt")
    

    That global interrupt halts the active prompt mid-sampling and moves it into history in an interrupted state. It does nothing to the pending queue — the next queued prompt starts running immediately after the interrupt completes. If that is not what you want, you must also clear the pending queue (see the next section).

    The newer and safer form accepts a prompt_id in the JSON body. This was added in PR #9607 specifically to close a race condition: without it, a timed interrupt could land on a different prompt that happened to start in the gap between your queue snapshot and your interrupt call. When you pass a prompt_id, the server checks whether that exact prompt is the one currently running and only interrupts if it is:

    requests.post(
        "http://127.0.0.1:8188/interrupt",
        json={"prompt_id": "1f2a3b4c-..."},
    )
    

    If the prompt is no longer running, the server logs “prompt is not currently running, skipping interrupt” and does nothing — which is exactly the behaviour you want for a targeted cancel. The trade-off is that this only ever touches the running prompt; it cannot reach anything pending.

    /queue Removes Pending Jobs and Can Wipe Everything

    For anything waiting in line, the route is POST /queue. It accepts a JSON body with either of two keys.

    To remove specific pending prompts, pass a list of prompt_id values under delete:

    requests.post(
        "http://127.0.0.1:8188/queue",
        json={"delete": ["id-1", "id-2"]},
    )
    

    To clear the entire pending queue in one shot, use clear:

    requests.post(
        "http://127.0.0.1:8188/queue",
        json={"clear": True},
    )
    

    A critical detail: /queue operates only on the pending list. If a prompt has already started running, deleting it from the queue does not stop the GPU work — the running prompt is not in the pending list at all. This is the single most common mistake in automation scripts: you call /queue delete on a prompt that just started, the call returns 200, and nothing appears to happen because the prompt is now running, not pending. To be safe, treat /queue delete and /interrupt as a pair rather than alternatives.

    /history Cleans Up Completed and Interrupted Records

    Once a job is finished — whether it completed normally, errored, or was interrupted — it lives in history. History entries are no longer on the GPU and do not block anything, but they still occupy server memory and clutter the GET /history output your script reads back. ComfyUI gives you a POST /history route to tidy up:

    # delete specific history entries
    requests.post(
        "http://127.0.0.1:8188/history",
        json={"delete": ["id-1"]},
    )
    
    # wipe all history
    requests.post(
        "http://127.0.0.1:8188/history",
        json={"clear": True},
    )
    

    The body shape mirrors /queue exactly, which makes the API easy to remember: delete accepts a list of ids, clear nukes everything. The key conceptual point is that /history is housekeeping, not cancellation. Do not reach for it when a job is still running or pending; it will not stop anything — it only removes records after the fact.

    The New /api/jobs/cancel Endpoint Is the Upgrade You Want

    For a long time, the “cancel one job no matter what state it’s in” use case had no clean answer. You had to snapshot /queue, figure out whether your target prompt_id was running or pending, then branch into /interrupt or /queue delete. The race condition in the middle was the source of the feature request captured in discussion #8835 — deleting a job from the frontend could silently fail because the job had already started running between your click and the request.

    ComfyUI now ships a state-agnostic, idempotent cancellation route that collapses all of that logic:

    requests.post("http://127.0.0.1:8188/api/jobs/1f2a3b4c-.../cancel")
    

    Under the hood it resolves the prompt’s state and does the right thing automatically: it interrupts if the job is running, dequeues if it is pending, and returns a benign {"cancelled": false} if the job already finished or the id is unknown. Because it is idempotent, you can safely fire it at a job you are no longer sure is still in flight without risking an error or interrupting the wrong thing.

    There is also a batch variant for multi-job teardown:

    requests.post(
        "http://127.0.0.1:8188/api/jobs/cancel",
        json={"job_ids": ["id-1", "id-2", "id-3"]},
    )
    

    The batch version is best-effort and idempotent too: each well-formed id is cancelled if it is running or pending, while finished or unknown ids are treated as no-ops rather than errors. A batch that is entirely no-ops still returns 200 with {"cancelled": false}. This is the endpoint to standardise on for any “cancel everything I just submitted” workflow.

    One compatibility note worth checking against your install: all of these routes are also available under an /api prefix (/api/interrupt, /api/queue, /api/history, and /api/jobs/.../cancel). The server auto-registers both the unprefixed and prefixed paths, so either spelling works — but if you reverse-proxy ComfyUI behind a gateway that routes on the /api prefix, using the prefixed forms keeps traffic on the correct path. The /api/jobs/* cancellation routes specifically live under the /api prefix.

    A Careful Cancel Routine That Never Interrupts the Wrong Job

    Putting the pieces together, here is a small, defensible pattern for cancelling one job by its prompt_id without collateral damage:

    import requests
    
    BASE = "http://127.0.0.1:8188"
    
    def cancel_job(prompt_id: str) -> bool:
        r = requests.post(f"{BASE}/api/jobs/{prompt_id}/cancel")
        r.raise_for_status()
        return r.json().get("cancelled", False)
    

    For a teardown in a script that might have queued several jobs, snapshot the id list first, then cancel the whole batch and clear history afterward:

    ids = [...]  # prompt_ids you submitted
    requests.post(f"{BASE}/api/jobs/cancel", json={"job_ids": ids})
    requests.post(f"{BASE}/history", json={"clear": True})
    

    Two habits will save you from most “the cancel didn’t work” reports. First, always capture the prompt_id from the POST /prompt response — it is the only stable handle across state transitions. Second, after cancelling, poll GET /queue and GET /history to confirm the prompt actually left the queue and did not silently restart, rather than trusting the HTTP 200 from the cancel call alone. See our guide on websocket events and /history polling for a deeper look at confirming job completion by state.

    Conclusion

    ComfyUI’s job-control API is small but easy to fumble if you conflate the five routes. Remember the mapping: /interrupt stops the single running prompt (optionally targeted by prompt_id), /queue deletes pending jobs or clears the whole backlog, /history deletes finished records, and the newer /api/jobs/{id}/cancel (plus its batch form) collapses state detection into one idempotent call. For new automation, prefer /api/jobs/{id}/cancel and keep the targeted /interrupt form for the narrow case where you must be certain you are stopping a specific in-flight prompt. Validate against the official ComfyUI server routes documentation and the exact version you have installed, since these cancellation helpers have landed progressively across recent releases.

  • ComfyUI “No Module Named” After pip install: The Two-Python Trap (venv vs python_embeded)

    The Symptom: You Installed It, But ComfyUI Still Says “No Module Named”

    You find a custom node on GitHub, follow the README, open a terminal, and run pip install some-package. The install completes with a green “Successfully installed” message. You restart ComfyUI, reload your workflow, and the node still fails with the same red error: ModuleNotFoundError: No module named 'some_package'.

    It feels like gaslighting. The package is clearly on your machine—you just watched pip download and install it. The reason it still fails is almost never the package itself. It’s that the python (or pip) you ran in the terminal is not the same Python interpreter that ComfyUI actually uses. ComfyUI runs in an isolated environment, and every custom node executes inside that same environment. Install into the wrong interpreter and nothing changes inside ComfyUI, no matter how many times you reinstall.

    This article walks through how to find out which Python ComfyUI is really using, how to install a dependency into the right one, and how to prove the fix worked. The steps are grounded in a real Linux setup I maintain, where this exact two-Python trap bites on a regular basis.

    Why There Are Two (or More) Pythons on Your Machine

    ComfyUI does not run on your operating system’s default Python. Since the project pulls in high-memory native libraries—PyTorch, CUDA, and friends—it deliberately isolates itself so those dependencies don’t fight with anything else you have installed. Depending on how you installed ComfyUI, that isolation takes one of a few concrete forms:

    • Portable (Windows): ships a self-contained python_embeded folder, typically at ComfyUI_windows_portable\python_embeded\python.exe.
    • Manual / comfy-cli install: creates a venv (Python virtual environment) or conda env, often at ComfyUI/venv/bin/python or inside a conda env named something like comfyenv.
    • Desktop app: manages its own bundled Python per-instance, so there is no separate interpreter for you to target at all.

    The official manual install guide is explicit about why this matters: a virtual environment prevents ComfyUI’s dependencies from conflicting with other system packages. There is a direct consequence users often miss, spelled out in the dependencies documentation: every custom node’s requirements.txt must be installed into ComfyUI’s own Python, not the default one on your PATH.

    Concrete Evidence From a Real Install

    On my own server, the mismatch is easy to reproduce. The system-wide interpreter is Python 3.12:

    # which python3
    /usr/bin/python3
    # python3 --version
    Python 3.12.3
    # python3 -c "import torch; print(torch.__version__)"
    2.13.0+cu130

    But the ComfyUI process was launched with a completely different interpreter inside a virtual environment:

    # ps aux | grep main.py
    ... /home/kings/ComfyUI/venv/bin/python /home/kings/ComfyUI/main.py --listen 0.0.0.0 --port 18188
    
    # /home/kings/ComfyUI/venv/bin/python --version
    Python 3.11.15
    
    # /home/kings/ComfyUI/venv/bin/python -c "import torch; print(torch.__version__, torch.version.cuda)"
    2.12.0.dev20260407+cu128 12.8

    These are two genuinely different torch builds on the same machine: system Python has 2.13.0+cu130 while the venv has 2.12.0.dev+cu128. Run pip install some-package in a plain terminal and it lands in /usr/local/lib/python3.12/dist-packages. ComfyUI, however, only ever imports from /home/kings/ComfyUI/venv/lib/python3.11/site-packages. The two never touch. Every “installed but still missing” error on a venv or portable install is this exact split.

    Step 1: Find the Python ComfyUI Actually Uses

    Before installing anything, identify the interpreter ComfyUI was started with. The most reliable signal is the running process, not whatever python happens to be on your PATH.

    On Linux / macOS

    ps aux | grep "[m]ain.py"

    The first token of the result is the interpreter. If it’s /path/to/ComfyUI/venv/bin/python, you have a venv. If it’s a conda path like /home/user/miniconda3/envs/comfyenv/bin/python, you have conda. If it’s a bare python3 or /usr/bin/python3, ComfyUI is running on your system interpreter (unusual for manual installs, but possible).

    On Windows (Portable)

    The portable build won’t typically show a separate PATH-based python. Instead, check for the python_embeded folder:

    dir ComfyUI_windows_portable\python_embeded\python.exe

    If that file exists, it is the interpreter—not the python in your C:\Python3xx or whatever else is on your PATH.

    You can also make ComfyUI tell you directly. A one-liner that prints the live interpreter avoids all guessing, and works regardless of OS:

    import sys; print(sys.executable)

    Run it from inside ComfyUI itself (for example, via a quick temporary custom node or the Python console in some builds) and it prints exactly which binary is executing your nodes.

    Step 2: Install Into the Right Environment

    Once you know the interpreter, install the missing dependency with that Python’s -m pip. The -m pip form is important: it guarantees the pip you invoke belongs to the exact interpreter you named, rather than depending on whichever pip script your shell resolves first.

    Venv (manual / comfy-cli install)

    # Linux / macOS
    /home/kings/ComfyUI/venv/bin/python -m pip install some-package
    
    # Windows (manual venv)
    ComfyUI\venv\Scripts\python.exe -m pip install some-package

    You do not need to activate the venv first. Pointing at its Python directly with -m pip is more precise than source venv/bin/activate followed by a bare pip install, because activation is state that can silently expire in a new terminal while the explicit path cannot.

    Portable (python_embeded)

    cd ComfyUI_windows_portable
    python_embeded\python.exe -m pip install some-package

    This is the exact pattern the official dependencies doc recommends for custom-node requirements, with the general form:

    python_embeded\python.exe -m pip install -r ComfyUI\custom_nodes\YourNode\requirements.txt

    Conda env

    conda activate comfyenv
    python -m pip install some-package

    With conda, make sure the active comfyenv is the one ComfyUI was launched under. A second, forgotten conda env is just another flavor of the same two-Python trap.

    Step 3: Prove the Fix (Don’t Just Restart)

    After installing, verify the package is importable from ComfyUI’s own interpreter before you restart and reload the workflow. This is the step most people skip, and it’s the difference between a fix and another round of guess-and-check.

    # Linux / macOS venv
    /home/kings/ComfyUI/venv/bin/python -c "import some_package; print(some_package.__version__)"
    
    # Windows portable
    python_embeded\python.exe -c "import some_package; print(some_package.__version__)"

    If the import prints a version without a ModuleNotFoundError, the package is now visible to ComfyUI. If it still errors, you installed into the wrong interpreter and the above command will tell you instantly—without the slower loop of restarting ComfyUI and re-running the workflow each time. Only after the import succeeds do you restart ComfyUI, then reload the node. Going forward, the maintainable habit is: on a manual install, always put a copy of that exact <venv>/bin/python -m pip install ... line in your notes next to the node name, so the next dependency has the correct target already written down.

    Common Pitfalls That Make the Trap Worse

    Several habits quietly convert this into a recurring problem rather than a one-time fix.

    • Running sudo pip install. Besides bypassing the environment, sudo often resets PATH and HOME, so the pip that runs is the system one, not the venv’s. It also writes into system-owned directories, which is the exact contamination a venv exists to prevent.
    • Trusting pip --version instead of python -m pip --version. The bare pip on your PATH may belong to yet another interpreter. The -m pip form removes the ambiguity.
    • Installing into the venv after ComfyUI is already running, then rebuilding the node without a real restart. Python caches imported modules; custom nodes import at startup. A dependency installed while ComfyUI is up will not be picked up until the process restarts.
    • Mixing PyTorch versions. If a custom node requires a newer torch than the venv has, running pip install torch inside the venv can silently downgrade/upgrade the very build ComfyUI depends on, breaking every other node. Prefer nodes whose requirements match ComfyUI’s own torch, and check pip show torch in the venv before and after.
    • Editing the wrong requirements.txt. Each custom node ships its own requirements.txt inside custom_nodes/<Node>/. Installing ComfyUI’s top-level requirements.txt does nothing for a node’s missing third-party package.

    Conclusion

    The “installed but still missing” error is almost never a broken package. It’s an environment boundary: ComfyUI runs an isolated Python (a venv, conda env, or portable python_embeded), and a bare pip install lands in your system interpreter instead. The fix is a three-step loop that takes about a minute:

    • Find the real interpreter with ps aux | grep main.py (or by locating python_embeded).
    • Install with -m pip through that exact interpreter, never a bare pip or sudo pip.
    • Prove it with a live import in ComfyUI’s own Python before restarting, so a failed fix surfaces in seconds instead of a full restart cycle.

    Once you’ve internalized “install into the interpreter ComfyUI uses, not the one on my PATH,” a whole class of custom-node import errors stops being a mystery. If you’re still stuck after confirming the package imports in the correct environment, the problem moves to the node itself—and that’s a different investigation, one worth tackling separately.

    For more on diagnosing broken custom nodes generally, see our walkthrough of ComfyUI “No Module Named” errors and how to diagnose broken custom nodes.

  • ComfyUI Headless API: Reclaim ~25% VRAM by Dropping the Browser UI

    Why Your VRAM Looks Full Even When ComfyUI Is Idle

    Open ComfyUI, load a checkpoint, generate one image, and then just sit there. Open nvidia-smi in a second terminal. The VRAM is still nearly as full as it was during the generation — even though nothing is running.

    That residual occupancy is not a leak. It is the ComfyUI browser frontend working exactly as designed. When the web UI starts, it keeps the models you loaded — the diffusion model, the text encoder, and the VAE — resident in GPU memory so that the next “Queue Prompt” click starts instantly. For an interactive workflow where you tweak a prompt and re-render every few seconds, that caching is a feature.

    But if you drive ComfyUI as a headless backend — submitting jobs over HTTP and not touching the browser at all — that always-on residency is pure waste. It is memory you are paying for while getting nothing back.

    How much exactly? A first-party test by Andrew Zhu on an RTX 5090 measured peak VRAM at 18 GB when running Krea-2-Turbo through the browser, dropping to 13.5 GB for the identical generation driven through a plain Python headless pipeline. That is a ~25% reduction, same model, same image quality (source). On a 16 GB card, 4+ GB is very often the difference between a workflow that fits and one that throws CUDA out of memory.

    The Two Modes: Browser UI vs. Headless Server

    ComfyUI is, at its core, an HTTP server. The node graph editor you see in a browser tab is just one client that talks to that server. The official documentation is explicit about this: “ComfyUI runs as an HTTP server on your machine by default. You can call it programmatically to submit workflows, upload files, download outputs, and monitor progress” (ComfyUI server documentation).

    That means the memory-hungry browser tab is completely optional. You can run the exact same backend in headless mode — no browser, no frontend — and submit the very same workflows as JSON over the HTTP API.

    The reason the browser wastes memory specifically comes down to *when* models get freed. There are two distinct behaviors:

    1. Browser UI: models stay resident in VRAM after a job completes, because the frontend is waiting for your next interaction.

    2. Headless API: you control the lifecycle. Once a workflow finishes and you move on, you can unload models (or let a force-offload node push them back to system RAM / disk) before submitting the next job.

    It is the same insight behind every serious local-AI deployment: system memory offloading is a *failsafe*, not an operating mode you want to lean on. The weight held in VRAM between runs is capacity you could be using for higher resolution, larger batches, or a bigger model.

    Step-by-Step: Convert a Working Workflow to Headless API Format

    Here is the reproducible path. It assumes you already have a working ComfyUI workflow in the browser and want to serve it without the UI.

    Step 1 — Export your workflow in API format

    The workflow JSON the API accepts is *not* the same file the browser saves. The browser’s “save” format stores UI metadata — node positions, colors, group sizes — that the API has no use for. The API format strips all of that out, leaving a smaller graph that is what /prompt actually consumes. The official docs describe this distinction in the Workflow API Format page.

    To convert: open your .json in the frontend via File → Load, then export with File → Export Workflow (API). Do not hand-convert the save format; the node catalog between the two formats diverges in ways that are easy to get wrong. (If your nodes “vanish” after submitting, that is almost always a save-format file being fed to the API — see this workflow.json vs workflow_api.json walkthrough.)

    Step 2 — Start the server in headless mode

    Launch ComfyUI with the frontend disabled:

    
    python main.py --listen 127.0.0.1 --port 8188
    

    You do not need a special --headless flag for memory purposes — headless here means “don’t open a browser tab,” not a separate binary. What matters is that *you* never keep the frontend open against this instance. Do not navigate to http://127.0.0.1:8188 in a browser. Submit everything over HTTP.

    For extra insurance, add your normal VRAM hygiene flags. If you are on a memory-constrained card, --disable-smart-memory prevents ComfyUI from preemptively loading more than it needs, and --reserve-vram carves out headroom for the OS. These flags are covered in detail in this VRAM launch-flag explainer.

    Step 3 — Submit the workflow over HTTP

    Post the API-format workflow to the /prompt endpoint as JSON with a "prompt" key wrapping your graph, plus a unique client_id:

    
    curl -X POST http://127.0.0.1:8188/prompt \
      -H "Content-Type: application/json" \
      -d '{"prompt": <your-api-format-graph>, "client_id": "headless-runner"}'
    

    A successful call returns a prompt_id. You then poll /history/{prompt_id} until the status flips to completed, then download the output images/videos from the returned filename fields. The full run-job-then-poll loop — which is exactly what you need for a reliable headless harness — is broken down step by step in this ComfyUI API automation harness guide.

    Step 4 — Unload between jobs

    This is the part that actually gives you the VRAM back. In an interactive browser session, the models never leave. In a headless loop, *you* decide when they do. Options:

    • Use a force_offload=True option on model-loading nodes (many custom-node video/image suites expose this) so weights return to system RAM immediately after the node runs.
    • Explicitly free VRAM between batches by letting the Python process that owns the next request manage the lifecycle, rather than keeping a single long-lived session.

    Either way, the point is the same: the browser’s “keep it warm” behavior is the thing you are opting out of.

    Common Pitfalls When You Go Headless

    Pitfall 1 — Submitting save-format JSON. The single most common “why did my nodes disappear” error. Always export via *Export Workflow (API)*, never the plain save file.

    Pitfall 2 — Expecting /prompt to be synchronous. It is not. /prompt enqueues the job and returns immediately with a prompt_id. If your script assumes the response body contains finished images, it will read garbage. You must poll /history or listen on the WebSocket (/ws?clientId=...).

    Pitfall 3 — Keeping a browser tab open “just to check progress.” The moment you load the frontend against your headless instance, you re-introduce the resident-model behavior you just removed. Monitor via nvidia-smi or the WebSocket events, not the web UI.

    Pitfall 4 — Forgetting the VRAM flags on a 16 GB card. Going headless helps, but it does not make a 24 GB checkpoint fit in 16 GB by itself. Headless reclaims the *idle* residency; it does not shrink the *peak* footprint of a single large forward pass. Combine headless mode with your model’s own offload/quantization path — for Wan 2.2 specifically, BlockSwap and offload_device are the levers, covered in this Wan 2.2 Animate 14B on 16GB VRAM guide.

    How to Verify the Reclaim Worked

    Do not trust the memory back until you measure it. Run nvidia-smi (or gpustat) at three checkpoints under your *headless* setup:

    1. Before submitting — right after the previous job completed and your script moved on. VRAM should be visibly lower than what it was mid-generation.

    2. Mid-generation — the peak. This tells you the true working-set size of one forward pass.

    3. After the next job completes — confirms the residency did not creep back up.

    The number you care about is the *idle* delta: browser-mode idle minus headless idle. That is the memory you reclaimed — on the RTX 5090 test it was roughly 4.5 GB, and on a 16 GB consumer card the same effect can turn a borderline OOM workflow into a stable one.

    Conclusion

    ComfyUI is an HTTP server first and a visual editor second. The browser tab is genuinely useful for prototyping — but once a workflow is stable, keeping the frontend open means handing a quarter of your VRAM to a UI you are not using. Export the API-format JSON, launch the server without a browser, submit over /prompt, and take back the idle residency by unloading between jobs. It is the single lowest-effort VRAM win available on memory-constrained hardware, and it costs nothing but a few lines of Python.

  • ComfyUI “Invalid workflow against zod schema”: Fix the Semantic Version Validation Error

    You download a popular workflow from Civitai, drag the JSON file into ComfyUI, and immediately get a toast that reads something like Invalid workflow against zod schema with a wall of text about Invalid semantic version (x.y.z) at "nodes[8].properties.ver". Nothing actually breaks — the workflow still runs — but the warning reappears every time you switch pages or reload, and it is maddening enough that users have spent entire days searching for a way to silence it.

    This error is real, it is well understood, and the fix is specific. It also happens to be a different problem from the better-known Prompt Outputs Failed Validation error, which is a server-side rejection when your API prompt does not match a node’s schema. This guide covers the frontend workflow-file validation instead: what Zod is doing, which fields trigger it, why it usually fires on nodes you did not write, and exactly how to make it go away without hiding a genuine problem.

    What the Zod Schema Validation Error Actually Is

    Zod is a TypeScript schema-validation library that ComfyUI’s frontend uses to check that a workflow file conforms to the expected structure before it is loaded into the graph editor. When you open a workflow, the frontend validates a set of rules: node types must be known, connections must reference real slots, and certain property fields must match a specific format. If a rule fails, Zod raises a collection of errors, and ComfyUI surfaces them all at once in the warning banner.

    The two fields that cause the overwhelming majority of these warnings are ver and aux_id, both of which live under a node’s properties object. They are not fields most users ever edit by hand, which is exactly why the error feels so confusing: the file loads fine, the graph renders fine, and yet the validator keeps complaining about data you never knowingly changed.

    The Two Fields That Trigger It

    The Semantic Version Field (ver)

    The ver property is meant to hold the version of the node pack that produced a given node. The validator expects it to be a valid semantic version string, matching a regex that is roughly:

    /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([\da-z-]+(?:\.[\da-z-]+)*))?(?:\+([\da-z-]+(?:\.[\da-z-]+)*))?$/

    This means a valid value looks like 1.2.3, 1.2.3-beta.1, or 1.2.3+metadata. Git commit hashes are also tolerated. The problem is that some custom node packs write a ver value that does not satisfy this pattern — a bare 1.0, a date string, a v1.2.3 with a leading v, or simply null. Any one of these makes Zod flag that specific node, and because the error is reported per-node, a single malformed value can produce a long list of Invalid semantic version entries, one for every node that inherited the bad value.

    The Auxiliary ID Field (aux_id)

    The aux_id property is expected to be a GitHub repository reference in the form owner/repo. The validator enforces the pattern github-user/repo-name. Some workflows ship node properties where aux_id is empty, is a bare username, or contains a full URL, and any of those triggers a separate set of Invalid format. Must be ‘github-user/repo-name’ errors.

    Why This Happens on Workflows You Downloaded

    The single most important thing to internalize is that these errors are almost never caused by anything you did. The offending values are baked into the node definitions by the custom-node authors when they export the workflow. A node pack that writes a non-semver ver string produces a workflow that loads fine inside that pack’s own authoring environment but trips the stricter validator in a newer ComfyUI frontend. In other words, the workflow was always slightly malformed; a recent frontend update simply started enforcing rules that were previously ignored.

    This is corroborated by the official issue tracker. ComfyUI issue #7309 documents exactly this pattern, with users reporting that the error appears on workflows they downloaded and disappears entirely after they disable validation. The validation for the ver field was introduced in frontend PR #2751, and the maintainers later noted the over-eager matching had been relaxed in current versions.

    Step by Step: Diagnose First, Then Decide

    Before you toggle anything, read the message closely, because the fix depends on which field is failing.

    1. Capture the full error text. Do not skim it. The warning lists the exact property path — for example nodes[8].properties.ver — which tells you both the node index and the offending field.
    2. Confirm it is ver or aux_id. Semantic-version and aux_id failures are the two dominant cases. If the message points at a different field, your workflow has a different structural problem and disabling validation may hide something real.
    3. Check whether execution is actually affected. In the vast majority of reports, the workflow runs normally despite the warning. If your graph queues and produces output, the validation is purely a frontend nuisance, not a blocker.
    4. Open the workflow JSON. If you want to fix the data at the source, locate the node by index and inspect its properties.ver or properties.aux_id value. You will typically see a value that clearly fails the pattern described above.

    How to Fix It

    Option 1: Correct the Field in the Workflow File

    The cleanest fix is to normalize the offending value in a text editor. Open the .json workflow, find the node, and change ver to a valid semantic version such as 1.0.0, or change aux_id to a proper owner/repo string. Save and reimport. This keeps validation enabled, which means any future structural problem will still be caught.

    Option 2: Disable Workflow Validation

    ComfyUI exposes a setting to turn off this validation entirely. In the Settings dialog, find the Workflow validation toggle and set it to disabled. This is the step users in issue #7309 confirmed removes the error immediately. The official troubleshooting guidance states it is acceptable to disable validation temporarily while you investigate. The caveat is the same as before: with validation off, genuinely broken workflows will no longer warn you, so treat this as a stopgap for workflows you trust rather than a permanent global default.

    Option 3: Update ComfyUI and the Affected Node Packs

    Because the overly strict matching was relaxed in later releases, simply being on a current frontend often clears the error. If a specific custom node is the source, update that pack from ComfyUI Manager so its exported workflows carry a compliant ver value going forward.

    Common Pitfalls

    • Confusing this with prompt_outputs_failed_validation. That is a server-side rejection of an API prompt with a valid execution record boundary, not a frontend file-import warning. The fixes do not overlap; see our guide on finding and fixing the invalid node in a failed prompt for that separate error.
    • Editing the wrong file. You may have both workflow.json and workflow_api.json saved. The Zod validation applies to the graph file you import into the editor, not the API-format export.
    • Assuming a leading v is fine. v1.2.3 fails the semver regex even though it looks reasonable to a human. Use 1.2.3.
    • Disabling validation and forgetting. If you later open a workflow with a real missing-node problem, you will not get the warning, and you may misdiagnose the resulting red nodes as something else. Re-enable validation once you have moved past the noisy workflows.

    Verification and Conclusion

    After applying a fix, reload the workflow and watch the banner. A corrected ver/aux_id value should eliminate the warning for the nodes that previously triggered it while keeping validation enabled. If you chose to disable validation, confirm the error is gone and then deliberately re-enable it on a workflow you know is clean to verify you have not masked a real issue.

    The Invalid workflow against zod schema error is, in the end, a data-hygiene signal rather than a runtime failure. The values that fail are usually inherited from custom-node exports, the validator was made stricter by a frontend change, and the practical fix is either a one-field edit in the JSON or a single settings toggle. Understanding the difference between this warning and the much more serious prompt-validation rejection is the key to not wasting a day on a problem that never affected your generation in the first place.

    For full context on how ComfyUI recommends ruling out custom-node issues, consult the official ComfyUI troubleshooting documentation and the dedicated custom-node troubleshooting guide.

  • ComfyUI Flux LoRA strength_clip Does Nothing: Why Your LoRA Strength Knob Is Half-Broken (and How to Check)

    You load a Flux LoRA, wire it into the Load LoRA node, and start dragging the strength_clip slider to tune the effect. Nothing changes. The image is identical from 0.0 to 1.0. You assume the LoRA is broken, delete it, or hunt for a different LoRA of the same character or style.

    In most cases the LoRA is fine. The real problem is a misunderstanding of what the two sliders actually control, and a quirk of how Flux LoRAs are usually trained. This article breaks down the “strength_model” vs “strength_clip” split, explains why strength_clip commonly appears to do nothing on Flux, and gives you a concrete way to check a LoRA instead of guessing.

    The Two Knobs: strength_model vs strength_clip

    A LoRA (Low-Rank Adaptation) is a set of small weight patches applied on top of existing models. In ComfyUI’s Load LoRA node, a single LoRA file can carry patches for two different models at once: the diffusion model (the UNet or, for Flux, the DiT transformer) and the text encoder (the CLIP model).

    That is why there are two strengths. The official ComfyUI FAQ puts it plainly:

    These separate values control the strength that the LoRA is applied separately to the CLIP model and the main MODEL. In most UIs adjusting the LoRA strength is only one number and setting the lora strength to 0.8 for example is the same as setting both strength_model and strength_clip to 0.8.

    • strength_model scales how strongly the patch affects the diffusion model. This is the heavy hitter — it changes how latents get denoised, which is where most of a style or character LoRA lives.
    • strength_clip scales how strongly the patch affects the text encoder. It changes how your prompt gets interpreted into conditioning, which usually produces a much subtler effect.

    When both parts exist in the LoRA, both sliders should do something. But here is the key insight: a LoRA does not have to contain both. Whether a slider does anything depends entirely on what the person who trained the LoRA actually trained.

    Why Flux LoRAs Usually Ignore strength_clip

    This is the heart of the confusion. ComfyUI issue #8050 documents exactly this scenario: a user got perfect results with the SDXL Load LoRA node, then switched to Flux with a DualCLIPLoader and found that strength_clip had zero effect on the output.

    The answer, from maintainer ltdrdata in that same thread, is that many Flux LoRA models are simply not trained on the CLIP model at all. Flux is a large DiT model with a separate text encoder; when people train a character or style LoRA, they frequently train only the diffusion transformer and leave the text encoder untouched. In that case the LoRA file contains no CLIP patch, and strength_clip scales an empty set of weights — so of course nothing changes.

    This is not a bug. It is a property of the specific LoRA file you downloaded. The distinction matters because it flips your intuition: on SD1.5/SDXL it is common to nudge strength_clip and see a visible shift in composition, but on Flux the same action frequently does nothing because there is nothing there to scale.

    How to Check Whether a LoRA Trained the Text Encoder

    Rather than guessing, you can inspect the LoRA directly. The same GitHub issue points to the LoRA Block Info node from the Impact Pack (ComfyUI-Impact-Pack). It reads the internal keys of a LoRA file and shows you which named modules the patch targets.

    The diagnostic step is simple:

    1. Install or enable the Inspire Pack (Impact Pack) if you do not already have it.
    2. Add a LoRA Block Info node and point it at your LoRA file.
    3. Look at the list of block keys it returns.

    If the keys point only at diffusion-model layers (for Flux, names like diffusion_model.* or transformer blocks), then the text encoder was not trained, and strength_clip will legitimately do nothing. If you see text_encoder.* or CLIP layer names in the list, then a CLIP patch is present and strength_clip should have an effect — and if it still does not, you can start looking for a real problem.

    ComfyUI Load LoRA node with strength_model and strength_clip sliders shown side by side in a dark node graph
    The Load LoRA node exposes two independent strength values because a single LoRA file can patch both the diffusion model and the text encoder.

    Common Traps That Mimic a Dead strength_clip

    Before you conclude the LoRA is malformed, rule out the surrounding mistakes that can also make the entire LoRA appear inert.

    • Base-model mismatch. A Flux LoRA only works on a Flux checkpoint. Loading a Flux LoRA against an SDXL or SD3.5 checkpoint produces garbage or no visible effect, on both strength knobs. Confirm the checkpoint family matches the LoRA.
    • Wrong loader node. Older tutorials show a generic Load LoRA node, but Flux workflows often need the LoRA wired into the correct model path after the CLIP loader. If the LoRA node is not actually in the diffusion-model chain, strength_model will also look dead.
    • Trigger words. Some LoRAs require an activation token in the prompt. If the token is missing, the LoRA may produce only a faint or zero response regardless of strength. Check the LoRA’s model page on Hugging Face or Civitai for required trigger words.
    • Strength set to an extreme. Values far above 1.0 (say 1.5+ on Flux) can blow out the latent and wash the image out, which reads as “the LoRA broke” when it actually worked too hard.

    None of these are specific to strength_clip, but all of them are easy to mistake for “this knob does nothing.” Isolate the variable: the only way to know strength_clip specifically is inert is to hold everything else constant and sweep only that one value.

    A Reproducible Isolation Test

    Set strength_model to a fixed, sensible value (usually 1.0), then render the same prompt at strength_clip = 0.0 and again at 1.0. Keep the seed, sampler, steps, CFG, and checkpoint identical.

    • If the two images are pixel-identical: the LoRA has no CLIP patch (or a negligible one), and strength_clip is doing exactly what it should — nothing. This is the expected Flux outcome for many files.
    • If the images differ: a CLIP patch exists and is active. Now the knob works, and you can tune it as intended.

    This two-render test takes a couple of minutes and removes the ambiguity completely. It is the same principle as the maintainers’ advice in the linked issue — check whether the text encoder was trained before assuming the UI is broken.

    When a CLIP Patch Exists But Still Does Nothing

    There is a rarer case worth covering: the LoRA does contain text_encoder.* keys, yet strength_clip still appears to have no visible effect. Two explanations are common.

    First, the CLIP patch may be visually subtle. Text-encoder patches shift conditioning, which often only nudges composition or style rather than producing an obvious “different image.” A slider that feels dead at a glance may still be changing the embedding vectors; compare the conditioning or inspect outputs at a fine granularity before declaring it inert.

    Second, a genuine wiring error can leave the loaded CLIP model disconnected from the LoRA. In Flux workflows that use a DualCLIPLoader (Flux uses two text encoders, CLIP-L and T5), the LoRA must be applied to the correct one. If the patch targets CLIP-L but is applied to a path that never reaches the prompt conditioning, the CLIP weight never takes effect. Trace the model chain and confirm the LoRA output actually reaches the sampler’s conditioning input.

    The official guidance is worth repeating: for Flux specifically, the ComfyUI_examples LoRA page shows the correct wiring pattern — the LoRA node patches the model output directly, and applying multiple LoRAs is just a matter of chaining additional Load LoRA nodes.

    Conclusion: The Knob Is Not Broken

    When a Flux LoRA ignores strength_clip, the overwhelmingly likely explanation is that the file was trained on the diffusion model only, and the text-encoder strength is scaling a patch that does not exist. This is normal, expected, and confirmable in seconds with the LoRA Block Info node or a two-render isolation test.

    Save yourself the detour: before replacing a “broken” LoRA, check whether it trained the text encoder at all. Tune strength_model for the character or style you want, leave strength_clip at a sensible default when no CLIP patch is present, and reserve real debugging for the cases where a CLIP patch exists and still fails to move the image.

    For the other silent failure that plagues Flux LoRAs — missing trigger words that make an entire LoRA do nothing — see our companion guide on fixing the silent LoRA no-op and missing trigger words.

  • ComfyUI Eats System RAM While VRAM Looks Free: Diagnose the Offload-vs-Leak OOM

    If you have ever watched your NVIDIA card sit at a lazy 30% VRAM while your Linux box grinds to a halt — mouse cursor stuttering, malloc failures, the whole machine frozen — you have hit one of the most confusing failures in local AI: ComfyUI exhausting system RAM while the GPU still looks half empty.

    The error is almost always reported as “CUDA out of memory” or a vanilla std::bad_alloc, which sends people straight to the wrong fix. They lower the image resolution, shrink the batch, or add --lowvram — and nothing improves, because the problem was never on the GPU in the first place.

    This article breaks down the symptom, shows you how to prove which tier of memory is actually dying, and gives you a concrete fix for each case. I write this from a first-hand snapshot of my own T2 machine: a 30GB-RAM box running ComfyUI 0.39.0 with an RTX 5060 Ti (16GB VRAM), which was sitting at 284MB of free RAM and 22GB of swap used while the GPU reported 10.1GB of VRAM free. That is the exact state this article exists to explain.

    The Symptom: VRAM Free, Machine Frozen

    The tell-tale signature is a mismatch between two readings taken at the same moment. Your GPU is fine, and your system is not. Here is what it looked like on my box mid-incident:

    • nvidia-smi / /system_stats: 10.1GB of 16.6GB VRAM free.
    • free -h: total 30Gi, used 30Gi, available 625Mi, swap 22Gi used of 43Gi.
    • ps aux --sort=-%mem: the ComfyUI/main.py process holding ~20.1GB RSS, 64% of all system memory.

    When you see that, stop tuning the sampler. The GPU was never the constraint — ComfyUI is holding model weights in ordinary CPU RAM, and it has run the machine into the swap file.

    The reason this is so common is that ComfyUI’s memory manager is aggressive about offloading. When a model (or a LoRA, a VAE, a text encoder like umt5-xxl, or a full video diffusion checkpoint) does not fit in VRAM at load time, ComfyUI keeps it in system RAM and streams pieces onto the GPU as needed. That is by design and usually correct. The subtle problem is that offloading and leaking produce the same free -h output — a huge RSS number — so you cannot tell which one you are looking at without one more step.

    How to Prove Which Tier of Memory Is Dying

    The single most useful diagnostic is to stop conflating VRAM and RAM, and read both at the same time. Three commands, run on the machine that is actually running ComfyUI:

    1. Read the GPU from ComfyUI’s own API, not from nvidia-smi.

    curl -s http://127.0.0.1:18188/system_stats

    Look at devices[0].vram_total vs vram_free. On my box this returned 16,610,820,096 total and 10,102,938,348 free. The API also reports torch_vram_total/torch_vram_free, which is the pooled-allocator view — the number to trust when a workflow is actively running under cudaMallocAsync.

    2. Read system RAM and swap.

    free -h

    If available is in the hundreds of MB while swap used is climbing into the gigabytes, your system is thrashing.

    3. Find the process responsible.

    ps aux --sort=-%mem | head

    On my box the top line was unambiguous: the venv Python running main.py --listen 0.0.0.0 --port 18188 at ~20GB RSS. If that same process is also the only thing consuming memory, you have a case of offloaded-weights residency; if the RSS climbs after every single queue run and never comes back down, you are looking at a leak.

    The distinction matters because the fixes are opposite. Offloading that simply never releases is a configuration problem (it is holding too much, or it is holding it after you are done). A leak that grows per-run requires either a restart cadence or a bug fix upstream.

    Rule Out a Real Leak Before Trying to “Fix” Offloading

    The healthiest instinct when you see 20GB of RSS is to assume it is a leak, but ComfyUI offloading is specifically designed to look exactly like that. A reliable test is to watch the RSS trend across two identical queue runs with no model changes between them:

    • Run the workflow once, note the RSS after it finishes.
    • Wait 30 seconds, run it again, note the RSS again.

    If RSS returns to roughly the same baseline after each run, you have offload residency — weights are parked in RAM and reused, which is expected. If RSS ratchets upward by the size of the model on every run and never returns, you have a leak, and the correct response is different: check whether a specific custom node is the cause (isolate it in a minimal graph), pin and update that node, and add a periodic restart while you wait for a fix. ComfyUI has an open, long-running issue about memory not being released after generation (Comfy-Org/ComfyUI#8298) that documents exactly this “RAM keeps growing after each run” behavior on high-RAM systems.

    One common trap: people read a full RAM line and assume offloading is not working, then disable offloading entirely. That is backwards. Full RAM with weights parked there is offloading working; the failure only begins when RAM plus swap is exhausted and the kernel starts the OOM killer. The goal is not zero RAM usage — it is staying under the ceiling where the machine remains responsive.

    Fixing the Offload Bloat Without Disabling It

    If you have confirmed offload residency (not a leak), the goal is to shrink the system RAM footprint and give ComfyUI a cleaner release path. In rough order of impact:

    Turn on Dynamic VRAM. ComfyUI’s dynamic VRAM mode improves how the manager moves tensors between VRAM and system RAM, which directly lowers the amount of RAM stuck holding offloaded weights and reduces OOM errors on tight machines. You enable it in the Settings → Memory → Dynamic VRAM toggle, or pass it at launch depending on your version.

    Understand that --lowvram is not the same lever. The launch flags --lowvram, --normalvram, --highvram, and --novram control how aggressively weights are moved onto the GPU, which changes VRAM pressure — but they do not, by themselves, cap how much system RAM an offloaded or partially-loaded model holds. If you are OOM-ing in CPU RAM, changing these flags will not save you and can even make the machine worse by forcing more residency in RAM. I cover the exact behavior of each flag in ComfyUI VRAM Launch Flags Explained.

    Close previews and stale generations. A huge and under-appreciated system-RAM sink is the accumulated preview / history backing store. If your browser tab has been open for hours and every queue prompt has rendered a live preview, that image data is sitting in RAM. Closing the browser tab (or the API client’s websocket that streams previews) and clearing the queue frees a surprising amount.

    Use an explicit release / free-memory node. If you are scripting or chaining many video segments, ComfyUI does not always return offloaded weights to a clean state between segments. Scheduling an explicit memory release — either through your driver script hitting the appropriate endpoint, or by restarting the ComfyUI process on a cadence — is the pragmatic solution most production pipelines settle on. My own Wan2.2 Animate setup on the same hardware leans on BlockSwap and force-offload precisely to keep a 16GB-class video model from thrashing; the full settings are in ComfyUI Wan2.2 Animate 14B on 16GB VRAM.

    Add swap deliberately — or remove the pressure first. A box that is sitting in 22GB of swap is past the point of tuning; it is thrashing. Give it headroom (close Chrome if it is running, stop the local LLM server you forgot about), then come back and re-measure free -h. Swap is a safety net, not a working tier — once you are depending on it for daily generation, every prompt becomes 10× slower.

    Verify You Actually Fixed It

    After applying whatever combination applies to your case, prove it with the same three commands you used to diagnose, and confirm the trend — not just a single lucky reading:

    • free -h shows available comfortably above the model footprint, and swap used is flat or falling, not growing.
    • Re-run the same workflow twice and confirm RSS returns to a stable baseline (offload) instead of ratcheting (leak).
    • /system_stats shows the VRAM pattern you expect, and the UI/API no longer throws “CUDA out of memory” or bad_alloc on a workload that previously died.

    The decisive test is responsiveness. A machine that is genuinely healthy will let you open a terminal and type while a generation is running. If the mouse still crawls during a prompt, you have not freed the system-RAM tier — you have only moved the problem around.

    Conclusion

    “CUDA out of memory” with a half-empty GPU is almost never a GPU problem. It is a system-RAM problem wearing a CUDA costume. The fix is to measure the right tier (VRAM via /system_stats, RAM via free -h, and the culprit via ps), distinguish offload residency from a genuine leak, and then apply the matching lever — Dynamic VRAM for the former, an isolated-node hunt and restart cadence for the latter. Get that right and a 30GB box with 16GB of VRAM goes back to being a perfectly capable local-AI workstation instead of a swap-bound slideshow.

    The memory management behavior is moving fast, so treat everything here as a snapshot to verify against the version you actually run. For current settings and the authoritative explanation of the memory manager, start with the official ComfyUI troubleshooting guide and the project’s memory-not-released issue thread.

  • ComfyUI Wan2.2 Keeps Eating RAM Until It Crashes? The –cache-none Fix, Explained

    There is a specific failure pattern that keeps showing up in ComfyUI threads about Wan2.2, and it is easy to misread. You load the 14B text-to-video or image-to-video workflow, generate one clip, and everything works. You generate a second clip, and RAM usage climbs. By the third or fourth run, the machine is thrashing into swap, the UI freezes, or the whole process dies with no useful error message — or worse, on Linux, the OS hard-freezes because the kernel overcommitted memory and ComfyUI filled it before the OOM killer could react.

    If your first instinct is to blame the model, the BlockSwap node, or your 16GB GPU, you are looking in the wrong place. The model being “too big” causes an out-of-VRAM error at load time. This is different: this is RAM that is never handed back to the OS after a completed generation. It is an execution-cache problem, not a model-size problem.

    Symptom: RAM Is Never Released After a Run

    The clearest description of this exact issue is in ComfyUI issue #9484, titled “RAM not being fully released after WAN 2.2 generation.” The reporter describes running “pretty much any generation” and seeing roughly 30GB of RAM reserved for no obvious purpose, with the leaked footprint growing on each subsequent run until RAM saturates and spills into swap. The timeline matters here: the problem reportedly became common around the time subgraph support landed in the daily builds, which made it easy to attribute the regression to the newer ComfyUI versions rather than to the model.

    Several commenters reproduced it under different hardware. One ran three ComfyUI instances — one per GPU — and watched a 128GB server start to OOM after only the second generation. Another confirmed it on an RTX 4090 with 32GB of system RAM, where ComfyUI would “crash a lot” without the fix. A third reported it on a 64GB machine with an RTX 3090: their Wan2.2 workflows that previously ran clean started breaking repeatedly, and ComfyUI would just stop with no error message, leaving the Windows console waiting on “Press any key to continue.”

    How to confirm it is this issue and not something else. Watch system RAM (not VRAM) across two or three generations. On Linux, run free -h between runs; on Windows, use Task Manager. If available memory drops after every completed run and does not come back up when the queue is empty, you are seeing the leak. The model being loaded and then intentionally kept warm for fast re-use is normal — but an ever-growing footprint that never returns to a baseline is the leak.

    The Real Cause: The Execution Cache, Not the Model

    The confusion that trips most people up is the word “cache.” ComfyUI has two completely separate caching systems, and they get blamed for each other’s problems all the time.

    • Model/diffusion-model cache — keeps loaded checkpoints in VRAM or RAM so you don’t re-read a 16GB file from disk on every run. This is what BlockSwap, --lowvram, and --normalvram interact with.
    • Execution cache — stores the output of individual nodes so that if you only change one parameter downstream, ComfyUI can re-use upstream results instead of re-running them. This is what the --cache-* flags control.

    The Wan2.2 leak lives in the second system. Video generation produces enormous intermediate tensors — latent frames across dozens of timesteps — and the execution cache holds onto them. On an image workflow, a cached latent is a few tens of megabytes and nobody notices. On a 14B video workflow producing dozens of frames, the cached intermediates are gigabytes, and they accumulate because the cache is not being reclaimed after the run completes.

    The official ComfyUI troubleshooting guide documents the lever directly: switching cache strategies. The startup-flags reference states that the cache-mode flags are mutually exclusive — you pick one of --cache-ram, --cache-classic, --high-ram, --cache-lru, or --cache-none. The key distinction, per the docs, is that --cache-none is “less RAM usage, but slower” because it re-executes every node on each run.

    This is why the fix is counter-intuitive. You are not changing how much VRAM Wan2.2 needs. You are telling ComfyUI to stop hoarding the gigabytes of intermediate results it would otherwise hang onto between runs.

    The Fix: Launch ComfyUI with –cache-none

    The fix reported working across hardware in issue #9484 is a single launch argument. Start ComfyUI like this instead of the default command:

    python main.py --cache-none

    On Windows Portable, edit the run_nvidia_gpu.bat (or whichever launcher you use) and add the flag to the python invocation line. On a headless Linux box or Docker setup, add it to whatever starts main.py. After restarting ComfyUI with the flag, run the same two or three Wan2.2 generations and watch free -h: available RAM should return close to the pre-run baseline after each queue drains, instead of ratcheting downward.

    The trade-off you are accepting. --cache-none means ComfyUI re-executes every node on each run, which costs some speed — most noticeable if your workflow already hit the same upstream nodes repeatedly. For a one-shot video generation, the difference is minor because the dominant cost is the diffusion model itself, not re-running a cheap preprocessor. For a long batch or a loop that re-uses upstream results, the slowdown is real. If --cache-none costs you too much speed, the middle ground is --cache-lru N, which caps how many node results are cached — it still bounds memory, but keeps a small hot cache for the nodes you re-touch most.

    One important correction from the issue thread: adding “clear VRAM” (or “Free Memory”) nodes to the workflow does not reliably fix this. Those nodes target VRAM/model cache, not the execution cache that is leaking. Multiple users confirmed the nodes had no effect while --cache-none did. Don’t waste time re-wiring the graph — the lever is a launch flag.

    Linux Users: The Overcommit Freeze Is a Separate Danger

    One comment in issue #9484 describes a failure mode worth flagging for Linux users specifically: the leak is worse on Linux because of memory overcommit. With default kernel settings (vm.overcommit_memory=0), Linux happily hands out more memory than physically exists, and when ComfyUI — already leaking — fills it all, the system hard-freezes before the OOM killer can react. The commenter tried tightening overcommit (sysctl vm.overcommit_memory=2 vm.overcommit_ratio=100) and found it caused ComfyUI to die when loading a second model instead — a different problem, not a fix.

    Net result: on Linux, fix the leak at the source with --cache-none rather than trying to fight it with kernel overcommit settings. If you are running multiple ComfyUI instances per GPU for throughput, this flag is effectively mandatory — the multi-instance OOM report on a 128GB server is the canonical warning case.

    How This Interacts With Wan2.2 Memory Optimization (and Why It’s Not the Same Thing)

    It is worth separating this RAM-leak fix from Comfy’s recent Wan2.2 VRAM optimizations so you don’t conflate the two. The official Wan2.2 memory-optimization update shipped ~10% less VRAM for VAE decoding and a major improvement for the 5B I2V model, plus new template workflows for the 14B models. Those changes reduce peak GPU memory during a run — a different axis entirely.

    You can think of the two levers this way:

    • VRAM (GPU): controlled by BlockSwap (blocks_to_swap), offload_device, quantized fp8 checkpoints, and the newer model-loader memory optimizations. This governs whether a 14B model fits on your card at all.
    • RAM (system): controlled by the execution-cache flags (--cache-none, --cache-lru). This governs whether your machine stays responsive across multiple runs.

    They are not substitutes. A 16GB card can load and run Wan2.2 14B using BlockSwap to page transformer blocks between VRAM and system RAM — that is a solved problem — and still fall over on the third generation because the execution cache is leaking RAM on top of the BlockSwap reservation. You need the VRAM trick and the RAM fix together for a workflow you actually run repeatedly.

    A Repeatable Verification Sequence

    Here is a concrete, reproducible check you can run in a few minutes to confirm whether you are affected and whether the fix took.

    1. Start ComfyUI normally and note system RAM baseline (free -h on Linux, Task Manager on Windows) with an empty queue.
    2. Run the same Wan2.2 generation three times in a row. Record available RAM after each run completes.
    3. If available RAM is shrinking after every run and not recovering, you have the leak.
    4. Restart ComfyUI with --cache-none (or add --cache-lru 8 if speed is a concern).
    5. Repeat the three generations. Available RAM should return close to the baseline after each queue drains.
    6. Confirm the trade-off: leave --cache-none in place if the per-run slowdown is acceptable; otherwise test --cache-lru with a value that keeps memory bounded but preserves your most-hit nodes.

    Conclusion

    If Wan2.2 runs fine once in ComfyUI but steadily eats RAM until the process crashes or your Linux box freezes, the model is almost certainly not the problem — the execution cache is. The fix that has been reported working across 32GB, 64GB, and 128GB systems is a single launch flag: python main.py --cache-none, or --cache-lru N as a bounded middle ground. Keep it separate from the VRAM-side optimizations (BlockSwap, quantized checkpoints) that govern whether the 14B model fits on your GPU in the first place: you generally need both for a workflow you run more than once.

    For a broader look at how the VRAM launch flags and offloading actually behave — the other half of this equation — see our earlier breakdown of ComfyUI VRAM launch flags.

  • ComfyUI “Missing Node” and “Import Failed”: A Complete Diagnostic Walkthrough

    The Symptom: Red Nodes and a Reload That Never Fixes

    You open a workflow you downloaded from Civitai or a friend’s chat, and half the graph is bright red. A banner tells you that nodes are missing, or the ComfyUI Manager reports “Import Failed” in red text next to a plugin name. You click “Install Missing Custom Nodes,” restart, and — sometimes — the red nodes are still there.

    This is far and away the most common support complaint in the ComfyUI community. The official troubleshooting guide opens with a blunt statement that most reported issues actually come from custom nodes, not from ComfyUI core. So before you reinstall anything, you need a reliable diagnostic sequence that tells you which of a handful of distinct failure modes you are actually hitting.

    The good news is that “missing node” is not a single problem. It is a symptom with a small set of well-understood causes, and each cause has a deterministic fix. This article walks through the diagnostic flow I use on a production ComfyUI install (66 custom node packages, running on an RTX 5060 Ti), with the exact commands and files to check at each stage.

    A quick reality check before you start: in many cases the node is not actually missing at all. It is merely failing to load, which ComfyUI reports as “missing” because the name never made it into the node registry. Your first job is to distinguish “not installed” from “installed but failing to import.” These have completely different fixes.

    The One Skill That Matters: Read the Actual Error

    ComfyUI prints import failures to its console (or terminal). The browser banner only tells you a node is missing — it does not tell you why. The real answer is always in the Python traceback written to the terminal where you launched ComfyUI.

    Start ComfyUI from a terminal so you can see that output:

    cd ~/ComfyUI
    python main.py

    Now trigger the failing load (restart, or click “Try fix” in the Manager). Scroll the terminal for a Traceback (most recent call last): block. The final line is the actual error class. Here is what each one means in practice:

    • ModuleNotFoundError: No module named 'torchvision' — a Python dependency is missing. The node package installed, but a library it imports is absent or the wrong version.
    • ImportError: cannot import name 'X' from 'Y' — a version mismatch: the custom node pins an API that changed in a dependency you have already upgraded.
    • OSError ... libcuda.so.1: cannot open shared object file — a native library or GPU driver issue, common on Linux (the official guide lists this under its Linux-specific section).
    • SyntaxError / IndentationError — the node code itself is broken for your current Python version; a plugin written for Python 3.10 behaviors can fail on 3.11+.

    This single step — actually reading the last line instead of the red banner — resolves a surprising number of cases because it tells you whether to install a pip package, pin a version, or touch nothing and just update the node.

    Verify Against the Registry, Not the Workflow File

    A workflow JSON stores names like KSamplerAdvanced or VHS_LoadVideo. When ComfyUI loads it, it looks each name up in its live node registry. You can confirm whether a name genuinely exists on your machine by checking the frontend’s object info endpoint:

    curl -s http://127.0.0.1:8188/object_info | python3 -c "import sys,json; d=json.load(sys.stdin); print('KSamplerAdvanced' in d, 'LoadImage' in d)"

    If a name prints False here, it is categorically not registered — either not installed, or installed but failing to import. If it prints True but the node is still red in the UI, you have a frontend/backend mismatch (the JavaScript side has an old shim), which a browser hard-refresh and a frontend rebuild usually clears.

    Diagnosing the Two “Missing” Cases

    Case 1: Genuinely Not Installed

    The workflow references a node whose package simply is not in custom_nodes/. This is the easy half. You have three install paths:

    • Via ComfyUI Manager (recommended): search the node name, click Install, restart.
    • Manually via git: cd custom_nodes && git clone <repo-url>, then restart.
    • Via git clone + pip install when the node has dependencies: clone, then pip install -r requirements.txt inside the node folder.

    On my own install, I run ComfyUI with a dedicated virtualenv (~/ComfyUI/venv, Python 3.11.15 in my case). Whichever install path you use, the dependencies must go into the same environment that runs ComfyUI, or you will get a ModuleNotFoundError the moment it loads.

    Case 2: Installed but Failing to Import

    This is the case the “Install Missing Custom Nodes” button cannot help, because the folder is already there. ComfyUI scans every directory in custom_nodes/ and tries to import it; an import that throws means the node never registers, and the UI shows it as missing.

    The loader logic is worth knowing because it explains a subtle trap. In ComfyUI’s node loader, a folder whose name ends in .disabled is skipped entirely — it is not imported at all. The same loader also consults a manager function (comfyui_manager.should_be_disabled()) to honor nodes you disabled through the Manager UI. This means:

    • A folder you renamed to MyNode.disabled to turn it off will show up as “missing” for any workflow that still references it — not because it is broken, but because you (or a previous session) disabled it.
    • Leftover junk folders with suffixes like .old-074513 (a timestamp backup pattern) are silently ignored. I actually found one of these on my live machine: a ComfyUI-GGUF.old-074513 directory sitting next to the active ComfyUI-GGUF. It is harmless, but it is also exactly the kind of directory that confuses people about why a node appears in the filesystem yet not in the UI.

    To isolate an import failure, disable everything except the problem node and observe. There are a few concrete actions:

    • Launch with only core nodes — temporarily move suspect folders out of custom_nodes/, or rename them with a .disabled suffix, then restart. If the crash disappears and comes back when you restore one folder, that folder is the culprit.
    • Read the __init__.py traceback — the failing import always prints a stack trace. The offending node folder’s name is usually visible in that trace’s file paths.
    • Check for a stale .disabled or .old-* twin — ls -d custom_nodes/*.old* custom_nodes/*.disabled* will reveal leftover folders that may be shadowing the real one in your mental model.

    The Manager Itself: A Source of “Import Failed”

    One frequently-overlooked cause of “Import Failed” is that ComfyUI-Manager itself is missing or out of date. The red “Import Failed” button people click most often is rendered by the Manager, so if the Manager can’t import the plugin, every plugin it tries to install or enable will report failure downstream.

    On my production machine, the Manager is not installed at all (a deliberate choice — I install custom nodes via git and manage dependencies directly in the venv). That is a completely valid setup, but it means:

    • There is no “Install Missing Custom Nodes” one-click button; missing-node workflows must be resolved by reading the workflow JSON’s custom_nodes list and cloning those repositories manually.
    • The comfyui-manager config directory (user/default/comfyui-manager/) does not exist, so any guide that assumes Manager-side disabling will not map to my environment.

    If your Manager shows persistent “Import Failed” for many nodes at once, the highest-value single action is to update the Manager itself (git pull inside custom_nodes/ComfyUI-Manager), then restart. A stale Manager frequently mis-reports import failures for plugins that import perfectly fine.

    Verification: When Is It Actually Fixed?

    A fix is not complete the moment the red nodes disappear. Confirm at three levels:

    • Registry check — re-run the object_info check above and confirm the node name now returns True.
    • Fresh generation — run the default workflow (or a minimal graph using just that node) end to end and confirm it executes without a “Prompt execution failed” dialog.
    • Console clean — the terminal should show no new Traceback lines on startup. A clean start is the real signal that the import now succeeds.

    If you are reporting a bug upstream, the official guide asks for exactly what a good diagnosis already gives you: the full console error, your list of installed custom nodes, the workflow JSON that reproduces it, and the recent changes you made. Gathering those as you go turns a frustrating “it just won’t load” session into a report that can actually be acted on.

    Conclusion

    “Missing node” and “Import Failed” feel like one problem but are really three: a node that isn’t installed, a node that’s installed but crashes on import, and a node that’s disabled (or shadowed by a stale folder) and thus skipped by the loader. The fix for each is different, which is why blindly clicking “Install Missing” so often fails to change anything.

    The discipline that pays off is embarrassingly simple: read the Python traceback in the terminal instead of the red banner in the browser. That one habit moves you from guessing to diagnosing, and it is the reason the official ComfyUI troubleshooting guide and the dedicated custom-node issues page both lead with “rule out custom nodes first.” If you are automating ComfyUI and want the diagnosis-before-queueing mindset baked into a script, the same principle shows up in my earlier walkthrough of submitting and polling ComfyUI’s /prompt API — check what the system actually reports before you assume the model or node is to blame.