DoMe Dynamics
AI Agent Systems
Zurueck zum Log
pythonsubprocesswindowsdeadlock

Python-subprocess-Deadlock: PIPE-Buffer voll, Prozess hängt ohne Fehlermeldung

Ein Script hing 14 Minuten ohne Fehler — der Pipe-Buffer war voll

Eine NotebookLM-Extraktion fror mitten im Lauf ein. Kein Fehler, kein Timeout, nur Stille. Die Ursache war kein Bug im Code, sondern ein voller Betriebssystem-Puffer — und eine subprocess-Annahme, die an großen Daten zerbricht.

Das Skript lief seit Wochen. Es rief ein externes Tool auf, las dessen JSON-Ausgabe und verarbeitete sie weiter — bei NotebookLM-Extraktionen, yt-dlp-Abrufen, allem, was strukturierte Antworten liefert. Dann, eines Tages, hing es. Vierzehn Minuten lang. Kein Fehler, kein Stacktrace, kein Timeout. Nur ein Prozess, der dastand und nichts tat.

Das Tückische an diesem Fehler ist, dass er nicht aussieht wie ein Fehler. Ein Crash hat einen Stacktrace, den man lesen kann. Ein Timeout hat eine Meldung. Ein Deadlock hat nichts — er ist die Abwesenheit von Fortschritt, und die ist schwer von „dauert halt lange" zu unterscheiden.

Was das Script tat

Der Aufruf war Standard: subprocess.run(cmd, capture_output=True, text=True). capture_output=True ist die bequeme Variante — sie sammelt stdout und stderr ein und gibt sie als fertige Strings zurück. Für die allermeisten Aufrufe ist das genau richtig: Befehl läuft, produziert ein bisschen Ausgabe, Python liest sie ein, fertig.

Der Unterschied an diesem Tag: Die Antwort war groß. Rund 314 KB JSON. Und genau bei dieser Größenordnung kippt das Verhalten.

Warum ein voller Puffer alles anhält

Zwischen einem Eltern- und einem Kindprozess liegt eine Pipe — ein Betriebssystem-Puffer mit begrenzter Größe. Unter Windows sind das je nach Konfiguration nur einige zehn Kilobyte. Der Kindprozess schreibt seine Ausgabe in diese Pipe; der Elternprozess liest sie heraus und macht Platz für mehr.

Das funktioniert, solange jemand auf der anderen Seite liest. subprocess.run mit capture_output=True liest aber erst, nachdem der Kindprozess fertig ist — vereinfacht gesagt: es wartet auf das Ende, dann holt es die Ausgabe ab. Bei kleiner Ausgabe ist das egal: alles passt in den Puffer, der Kindprozess schreibt fertig, beendet sich, der Elternprozess liest. Bei großer Ausgabe entsteht ein Patt: Der Kindprozess füllt den Puffer, will weiterschreiben, kann aber nicht, weil niemand abräumt — also blockiert er. Der Elternprozess wartet darauf, dass der Kindprozess sich beendet — was nie passiert, weil der Kindprozess am Schreiben hängt. Beide warten aufeinander. Das ist der Deadlock.

Verschärft wird es, wenn stdout und stderr gleichzeitig volllaufen. Dann ist jede Pipe für sich ein potenzieller Hängepunkt, und es reicht, dass eine von beiden den Puffer füllt.

Die vierzehn Minuten waren übrigens kein „es lief langsam" — es lief gar nicht. Der Prozess wäre auch nach vierzehn Stunden noch dagestanden.

Die Lösung: nicht in den Speicher, sondern auf die Platte

Der Fix ist unspektakulär: Bei potenziell großer Ausgabe nicht capture_output=True verwenden, sondern stdout auf eine temporäre Datei umleiten — subprocess.run(cmd, stdout=open(tmp, "wb")). Das Betriebssystem schreibt direkt in die Datei, der Puffer läuft nie voll, der Kindprozess blockiert nie. Danach liest Python die Datei ein.

Das ist langsamer als der Speicher-Weg — ein zusätzlicher Schreib-/Lese-Vorgang auf die Platte. Aber „langsamer und korrekt" schlägt „schnell und hängend" in jeder Bilanz. Die saubere, stdlib-korrekte Alternative wären nebenläufige Threads, die beide Pipes parallel leeren (genau das tut subprocess.communicate() intern) — aber für ein Tool, das ohnehin große Dateien produziert, ist die Temp-Datei der einfachere und robustere Weg.

Die Faustregel, die daraus wurde: Sobald eine Subprozess-Ausgabe potenziell über etwa 50 KB gehen kann, ist capture_output=True ein Risiko, kein Komfort.

Stille ist die schlimmste Fehlermeldung

Die eigentliche Lektion ist weniger technisch als diagnostisch. Dieser Bug war so lange unentdeckt, weil er sich nur bei großen Eingaben zeigte — und große Eingaben waren der Ausnahmefall. „Funktioniert mit kleinen Daten" ist ein verräterisches grünes Licht: Es verdeckt jede Klasse von Fehlern, die erst an der Pufferung, am Speicher oder an der Datenmenge hängt.

Und weil der Fehler sich als Stille äußert, ist er teuer zu diagnostizieren. Bei einem Crash sucht man nach der Ursache. Bei einem Hang sucht man erst einmal nach der Frage, ob überhaupt etwas kaputt ist. Die Antwort fand sich nicht im eigenen Code, sondern im Verständnis dessen, was zwischen den Prozessen passiert — in der Pipe, die man normalerweise nie sieht.

Was offen ist

Der Temp-Datei-Fix ist robust, aber er ist eine Behandlung des Symptoms am Aufrufpunkt. Die saubere Lösung wäre, große Antworten gar nicht erst als ein einzelnes JSON-Blob über die Pipe zu schieben, sondern zu streamen — zeilenweise oder in Chunks, sodass nie eine große Menge auf einmal im Puffer liegt. Das verlangt aber, dass auch das aufgerufene Tool Streaming unterstützt, und nicht jedes tut das.

Bis dahin gilt die einfache Regel: Bei allem, was groß werden kann, geht die Ausgabe auf die Platte, nicht in den Speicher. Es ist nicht elegant. Aber es hängt nicht.


Ein Prozess, der ohne Fehler einfach stehenbleibt, gehört zu den teuersten Fehlerbildern — der Puffer gerät meist als Letztes in Verdacht. Wenn dir das auch schon passiert ist, schreib mir.