Was 100 eigene ComfyUI-Nodes in der Produktion tun und wie sie ausgeliefert werden
Autorin: Anastasiia Butova, Ingenieurin für ComfyUI und Diffusionsmodelle, Belgrad · veröffentlicht 2026-09-23
Übersetzung der englischen Fassung: English
Ich habe 100 eigene ComfyUI-Nodes in 16 Packs geschrieben. Die Zahl wurde am 15. September 2026 aus dem Registrierungscode jedes Packs gezählt, nicht aus dem Gedächtnis. Die meisten davon sind eines von drei Dingen: ein Modell, verpackt, damit es im Graph läuft, eine Entscheidung, die der Graph treffen muss, oder eine Bildoperation, die kein bestehendes Pack so macht, wie unsere Pipeline es braucht. Unten stehen echte Beispiele für jede Sorte, dann der Teil, der meist übersprungen wird: wie ein Node in ein Produktions-Image kommt und was passiert, wenn der falsche auf einem Server landet.
Woher die Zahl stammt
Ein Skript liest die Python-Quellen jedes Packs und sammelt die Node-IDs in den drei Registrierungsformen, die ComfyUI akzeptiert. Es fand 104 Registrierungen und 100 verschiedene Nodes, denn vier Hilfsnodes werden von zwei Packs registriert. Wenn eine Quelle nicht gelesen werden kann, beendet sich das Skript mit Code 2, und dann wird die Zahl nicht veröffentlicht. Die größten Packs sind ein allgemeines mit 32 Nodes (Routing, Gates, Masken, Rauschen, Farbe, Kontaktbögen), ein SAM-3-Pack für Edelsteine mit 20 und ein Pack für Kacheln und Frequenzen mit 19. Zehn kleine Packs enthalten jeweils einen bis drei Nodes, meist Einzelmodell-Wrapper. Die vollständige Tabelle steht auf /comfyui/.
Die Repositorys sind privat, und mehrere Packs wurden im Rahmen von Kunden- und Arbeitgeberprojekten geschrieben (die Repository-Beschreibung des größten lautet noch „by Glam AI“), deshalb beschreibe ich sie, statt sie zu verlinken. Vor dieser Zählung stand auf der Seite „100+ Nodes, Open Source“. Die Zahl hielt stand; „Open Source“ nicht.
Ein Modell, verpackt, damit es im Graph läuft
NAFNet, SwinIR und HYPIR restaurieren, SAM 3 segmentiert Autoteile und Edelsteine, DINOv3 vermisst Autos und klassifiziert, YOLO26 erkennt. Als Nodes laufen sie im selben Job wie die Generierung, auf derselben GPU, ohne Umweg über einen anderen Dienst.
Das Modell aufzurufen ist beim Wrapper der einfache Teil. Der Rest besteht darin, dass es auf Hardware funktioniert, die der Autor des Graphen nie gesehen hat. Der SwinIR-Node hat GPU-Profile, die Kachelgröße und Batch festlegen, und das Workflow-JSON enthielt das Profil, das sein Autor gewählt hatte, etwa H200 mit 141 GB. Auf RunPod Serverless verteilt der Scheduler, was der Endpoint-Pool gerade hat, von mehreren H100-Varianten bis zu einer RTX PRO 6000 Blackwell. Das falsche Profil bedeutete Out-of-Memory-Fehler, Wiederholungen und Jobs, die das 30-Minuten-Limit überschritten. Seit Version 1.6.0 erkennt der Node die Karte selbst und ignoriert diese Einstellung.
Früher fiel er bei Out-of-Memory auf die CPU zurück, wo eine Kachel etwa 40 Sekunden dauert und ein ganzer Job Stunden. Diesen Rückfall habe ich in 1.5.8 entfernt: Jetzt bricht der Node laut ab, mit einer Zusammenfassung des Speichers. Ein langsamer Erfolg, den niemand bemerkt, ist schlechter als ein Fehler, den jemand behebt.
Entscheidungen, die der Graph treffen muss
Ein ComfyUI-Graph kennt kein if. Unsere verzweigen so. Ein Router-Node legt das echte Bild auf den Ausgang, den er gewählt hat, und einen schwarzen 1×1-Platzhalter auf jeden anderen. Am Anfang jedes Zweigs sitzt ein Gate:
def run(self, image):
from comfy_execution.graph_utils import ExecutionBlocker
if not torch.is_tensor(image):
print(f"[HappyinImageGate] NOT a tensor → BLOCK")
return (ExecutionBlocker(None),)
h, w = image.shape[1], image.shape[2]
if h <= 1 or w <= 1:
print(f"[HappyinImageGate] {w}x{h} → BLOCK")
return (ExecutionBlocker(None),)
print(f"[HappyinImageGate] {w}x{h} → PASS")
return (image,)
ExecutionBlocker sagt ComfyUI, dass nichts nachgelagert davon laufen soll, sodass ein nicht gewählter Zweig keine GPU kostet. Am Ende jedes Zweigs fragt ein Platzhalter-Node mit einem Lazy-Eingang entweder nach dem Ergebnis des Zweigs oder reicht, falls sein Zweig nicht gewählt wurde, das 1×1 ungefragt weiter; ein Merge-Node gibt dann das erste echte Bild weiter. Ohne den Lazy-Eingang würde der Blocker bis zum Merge gelangen und alles danach blockieren.
Drei der Router:
- NSFW und Verschlechterung. Ein NSFW-Flag plus ein Qualitätsurteil aus Pixelmetriken (JPEG-Blockigkeit aus der DCT, Farbbanding, Chroma-Rauschen, Ringing, Unschärfe) und ein CLIP-basierter Ästhetikwert. Vier Bildausgänge, von
nsfw_bad_qualitybissafe_good_quality, sodass ein verschlechtertes Bild in einen Hochskalierungszweig geht und ein sauberes in einen leichteren. - Nahaufnahme. Gesichtsabdeckung, auf zwei Arten gemessen, mit MediaPipe-Gesichtshautsegmentierung und der Box eines Gesichtsdetektors; das stärkere Signal gewinnt.
- Florence-Prompt-Router. In einem Workflow für Schmuckretusche beschreibt Florence-2 das Bild, und wenn die Beschreibung einen Stein nennt, wird der Prompt für Edelsteine verwendet.
Der Florence-Router war der, von dem ich am meisten gelernt habe. Bei 30 echten Kundenbildern nannte Florence die Steine in allen 18 Bildern, die welche hatten, und trotzdem wurde der Steinzweig nur 17 Mal gewählt. Alle drei Ursachen lagen im Abgleich, keine im Modell. Pluralformen: diamonds passt nicht auf \bdiamond\b. Verneinungen: „There are 0 gemstones“ enthält das Substantiv. Formen: „diamond-shaped design“ ist ein Muster, kein Stein. Die naheliegende Korrektur, ein optionales s, war schlechter: Sie feuerte bei allen 30 Bildern, weil die verneinten Sätze das Substantiv ebenfalls enthalten. Die Reihenfolge zählt: zuerst Verneinungen entfernen, dann Formangaben entfernen, dann mit Pluralen abgleichen. Und „cut“ durfte nicht auf die Formliste, denn „brilliant-cut stone“ benennt einen echten Stein.
Auf einem gemeinsamen Satz von 22 Beschreibungen traf der alte Abgleich 13 richtig, ein erster Korrekturvorschlag 17 und die endgültige Version 22. Der Teil, den ich in jedem Router behalten würde: Der Abgleich wurde aus dem Node in eine einfache Funktion verschoben, getestet an gespeicherten Beschreibungen, ohne Florence und ohne GPU. Wie die Commit-Nachricht es ausdrückt: „a regression now costs a second instead of a graph run“.
Bildoperationen, die kein Pack auf unsere Art macht
Die dritte Sorte ist gewöhnliche Bildverarbeitung, bei der bestehende Nodes etwas Ähnliches taten, aber nicht das, was die Pipeline brauchte:
- Rauschen. Ein Schätzer (Laplace plus MAD über das glatteste Zehntel von 64×64-Patches, minus einen kleinen Sockelwert, damit ein sauberes Bild nicht als verrauscht gilt) und ein Korn-Abgleich, der das Korn eines Referenzbildes auf das Zielbild überträgt.
- Ausrichtung. Ausrichtung per optischem Fluss mit einem Verformungs-Fallback, danach Frequenzmischung, damit zwei Versionen eines Bildes übereinanderliegen, bevor sie gemischt werden.
- Größen. Ein Latent-Größen-Snap, der ein Bild auf die nächste Größe in der Tabelle des gewählten Modells streckt, jede Seite ein Vielfaches von 16, statt es zuzuschneiden.
- Kacheln. Kachel-Zerlegung und -Zusammensetzung sowie Frequenzzerlegung für Produktfotos mit 6K–10K Pixeln; die Nähte, die beim Kacheln entstehen, stehen in einem eigenen Beitrag.
- Autos. Für mashinki ein Node, der die Hintergrundplatte so neigt, dass sie zur Rollneigung der Kabinenkamera passt.
Wie ein Node in ein Produktions-Image kommt
Für OIS.Gold beginnt ein Produktions-Image mit einem vendorierten Bundle: 21 Packs, 14 öffentliche und 7 private, ohne ihre .git-Ordner hineinkopiert. Der Build schreibt den Commit des Bundles als .vendor-sha in das Image, sodass ein Image sagen kann, aus welchem Bundle es gebaut wurde. Aktualisieren ist eine bewusste Handlung: Packs auffrischen, die Workflows gegen den neuen Stand laufen lassen, committen, neu bauen. Bei einem vendorierten Pack kann eine Änderung auf dem Hauptzweig eines anderen nicht über Nacht in die Produktion gelangen.
Das Bundle wurde am 23. April zusammengestellt, nach einem Verlust. Vier Tage zuvor war ein GPU-Host beendet worden, und der Snapshot seines Containers enthielt das äußere Image, aber nicht die eigenen Nodes. Ein Node, der Florence-Router von oben, ging dabei verloren. Ich habe ihn am 27. April aus seinen Ein- und Ausgaben neu gebaut, so wie sie im Produktions-Workflow-JSON festgehalten waren.
Die Vendor-Regel hat eine Ausnahme, und die nenne ich lieber, als sie glattzubügeln. Packs in aktiver Entwicklung, drei meiner eigenen und einige Drittanbieter-Packs im Blackwell-Image, werden beim Build zusätzlich vom Hauptzweig auf das Bundle geklont, mit einem optionalen Commit-Pin für den Fall, dass ein Zweig zurückfällt. Für diese Packs ist der Hauptzweig Produktion, auch der eines anderen. Deshalb bekam mein größtes Pack im August CI, und die Workflow-Datei sagt es in einem Satz:
Was auf master landet, erreicht den nächsten Worker-Rebuild, also muss master grün bleiben.
Die Tests installieren absichtlich kein Torch. Sie decken ab, was ohne Florence und ohne GPU läuft, und genau dort lagen die Router-Fehler.
Testen heißt nicht Beurteilen. Ein neuer Checkpoint oder eine Node-Änderung wird auf einem Bogen gegen die vorherige Version beurteilt, mit denselben eingefrorenen Prompts und demselben Seed in jedem Feld. Die wowfaceaibot-Seite zeigt zwei Bögen dieser Art aus meinen eigenen Charakter-LoRA-Läufen.
Ein Pack läuft beim Import
Zwei Module in meinem größten Pack waren nie Nodes. Sie hatten keine Eingänge und keine Registrierung und existierten nur wegen ihrer Nebenwirkungen beim Import: Eines umhüllte PromptExecutor.execute von ComfyUI, um für jeden neuen Prompt das PNG und das Workflow-JSON zu speichern, das andere patchte den Speicherpfad, um dem Namen des Containers ein Präfix voranzustellen. Auf einem Entwicklungsrechner nützlich. In der Serverless-Produktion schrieben flüchtige Worker einen Verlauf, den niemand las, und eine Umhüllung mit eigenem try/except um den Executor konnte einen echten Workflow-Fehler verbergen. Im Mai zogen sie in ein eigenes Repository um, das der Produktions-Build nie klont, und jede dauerhafte Nebenwirkung lebt seitdem dort.
Der Florence-Router lehrte die andere Hälfte. Er lud Florence-2 mit trust_remote_code=True, sodass auf einem frischen Worker die transformers-Bibliothek den Python-Code des Modells vom Hugging Face Hub holte und ausführte. Im Mai funktionierte dieser Code mit der installierten transformers-Version nicht mehr. Der Router fing den Fehler ab und gab eine leere Beschreibung zurück, und die Kaskade endete damit, dass Worker ein flaches PNG statt der geschichteten PSD lieferten, mit der die Retuscheure arbeiten. Die Korrektur zielte auf unseren eigenen Node: eine lokale Kopie, geladen mit local_files_only, und transformers festgepinnt. Ein globales HF_HUB_OFFLINE=1 hielt einen Tag, weil es auch einen legitimen Download eines Community-Nodes blockierte. Im August, auf einer Kiste mit transformers 5, wechselte der Node auf den Modellcode im ComfyUI-Florence2-Pack, der den Pfad für Remote-Code nie nimmt. Dieser zweite Fehler war tagelang hinter derselben leeren Beschreibung verborgen gewesen.
Ein Custom Node ist Python mit den Rechten des Servers
Das ist der Vorfall hinter der Sicherheitszeile auf /comfyui/. Im Februar 2026 minte ein ComfyUI-Container auf einem H100-Server, mit dem ich arbeitete, Monero mit einer XMRig-Variante; der Host selbst war sauber.
Der ComfyUI-Port des Containers war ohne Authentifizierung im Internet offen. Jemand schickte ihm einen Workflow mit SRL Eval aus srl-nodes, einem öffentlichen Pack. Der Node tut, was seine Dokumentation sagt: Er wertet jedes Python aus, das man ihm gibt, und ein Kommentar in seinem Quelltext ergänzt „ComfyUI isn't secure to begin with“. In einer geschlossenen Kiste ist das eine Bequemlichkeit. Auf einem offenen Port ist es eine Remote-Shell.
Der Code des Angreifers schrieb /opt/ComfyUI/execution.py um: Die saubere Datei hat etwa 1.282 Zeilen, infizierte Kopien liefen auf 1.700 bis 2.100 und mehr. Bei jedem Workflow-Lauf pausierte der injizierte Code den Miner, damit die Generierung die CPU bekam, und startete ihn danach wieder; wenn das Binary gelöscht worden war, lud er es erneut herunter. Die Binaries lagen in etwa 27 Verzeichnissen unter Namen, die wie Systemdienste aussehen, bei jeder Infektion anders: drei Wellen in fünf Tagen, am 13., 17. und 18. Februar. Das Löschen des Packs half nicht, denn ComfyUI-Manager hatte einen Snapshot der installierten Nodes behalten, sah beim Neustart, dass srl-nodes fehlte, und klonte es zurück. Die Neuinfektion kam binnen Minuten.
Ich habe das Erkennungs- und Entfernungs-Runbook geschrieben, mit einem Prüfskript und einem Aufräumskript. Die Reihenfolge ist entscheidend:
- Den Port von außen zuerst in der Kette
DOCKER-USERvon iptables sperren, sonst ist alles Folgende binnen Minuten wieder rückgängig gemacht. Die Regeln überleben keinen Neustart des Docker-Daemons, außer man speichert sie. - Jeden Prozess im Container beenden außer ComfyUI selbst.
- Das Pack löschen und jeden Manager-Snapshot, der es auflistet.
- Die Binaries löschen und danach nach jeder ausführbaren Datei außerhalb der bekannten Tool-Verzeichnisse suchen.
execution.pyaus Git wiederherstellen, der einzig verlässliche Weg, dann die Zeilenzahl prüfen und null Treffer für die Marker des Miners.- Neu starten, bestätigen, dass das Pack nicht zurückgekehrt ist, und das saubere Image committen.
Das Prüfskript schaut auf fünf Dinge: Miner-Strings in execution.py, zusätzliche Prozesse, unerwartete ausführbare Dateien, Verbindungen zum Pool und ob das Pack zurück ist. Das Runbook listet auch auf, was nicht gelöscht werden darf: Ein unbekanntes Paket in diesem Container war legitime GPU-Speicherverwaltung.
Beim erneuten Lesen des Prüfskripts für diesen Beitrag fand ich einen Fehler. Kann sich SSH nicht verbinden, kommen alle Zählungen leer zurück, jeder Test fällt auf OK, und das Skript gibt RESULT: CLEAN aus. Eine Prüfung, die nicht laufen konnte, muss das als drittes Ergebnis sagen.
Im April 2026 veröffentlichte Censys eine Analyse einer Kampagne gegen öffentlich erreichbares ComfyUI. SrlEval steht auf ihrer Liste ausgenutzter Nodes, und wenn kein verwundbarer Node installiert ist, installiert der Scanner selbst einen über ComfyUI-Manager. Derselbe Node, und der Manager wieder als Installateur.
Was das für das Schreiben und Ausliefern von Nodes bedeutet
- Die Eingaben eines Nodes sind eine API. Führt ein Node Code aus, liest er einen Pfad oder lädt etwas anhand seiner Eingaben, dann kontrolliert, wer den Graphen kontrolliert, genau das.
- ComfyUI bleibt vom öffentlichen Internet ferngehalten, hinter einem schmalen Dienst; wie das für das Autostudio auf dieser Seite funktioniert, steht im begleitenden Beitrag.
- Nodes kommen zur Build-Zeit, aus einer Liste in Git, und ein Pin ist besser als ein Zweig. Code, der zur Laufzeit kommt, etwa ein Snapshot, der ein Pack neu installiert, oder
trust_remote_code, das Python holt, ist Code, den niemand bewusst ausgeliefert hat. - Wenn die Produktion aus einem Zweig baut, bekommt dieser Zweig CI, mit den GPU-freien Tests im Pack.
Wenn du einen Node für einen Schritt brauchst, den kein bestehendes Pack abdeckt, oder eine Pipeline, die sicher auszuliefern ist, steht auf /comfyui/, wie ich arbeite.