
Le booléen de backpressure est le mécanisme, pas le stream
Les streams de Node.js portent un signal de backpressure — la valeur booléenne renvoyée par .write() — et la majeure partie du code en production ne le lit jamais. L'article de Master.dev sur les fuites de mémoire des streams Node.js parcourt la conséquence : des pods qui montent à 3,8 Go et se font tuer par OOM parce qu'un transform appelait .write() sur chaque ligne et ignorait le false qui demandait de ralentir (Master.dev, « Your Node.js Streams Aren't Backpressuring. They're Silently Eating Your Memory. », 2026). La panne n'est pas un bug du runtime. C'est un comportement correct, exécuté par du code qui n'a jamais mesuré le seul signal qui aurait fait tenir la propriété.
C'est toute l'histoire, et c'est celle que nous continuons de raconter chez Everythink : une propriété est garantie exactement quand son mécanisme est implémenté et mesuré. Le booléen de backpressure est le mécanisme. La boucle for await qui ne le vérifie jamais est la mesure manquante. La mémoire bornée n'est pas une caractéristique des streams ; c'est une propriété qui émerge d'un protocole que quelqu'un doit honorer.
Le signal existe. Personne ne le lit.
L'article de Master.dev cadre la fuite comme l'écart entre deux modèles mentaux : la version tutorielle (« les streams traitent les données morceau par morceau, donc vous ne chargez jamais le fichier entier ») et la réalité opérationnelle (« les streams vous donnent les outils pour vous protéger ; ils ne vous protègent pas »). L'auteur est précis sur ce que le runtime fait et ne fait pas : Node.js ne lève pas d'exception, ne met pas en pause, ne tue pas le flux quand un producteur ignore la backpressure. Il continue d'accepter les données, continue d'allouer du tas et continue jusqu'à ce que V8 manque d'espace.
C'est la partie qui surprend les ingénieurs arrivés aux streams par l'abstraction. L'abstraction annonce une garantie — « vous ne chargez jamais le fichier entier en mémoire » — et décharge silencieusement le travail de la maintenir sur un seul booléen que l'API ne vous oblige pas à lire. highWaterMark n'est pas une limite. C'est le seuil auquel .write() renvoie false. Pas d'exception, pas de pause automatique, pas de disjoncteur. Les quatre lignes que l'article montre comme correctif — vérifier la valeur de retour, await once(writable, "drain") si faux — sont tout le motif, et leur absence est toute la fuite.
[PERSONAL EXPERIENCE] J'ai lu ce bug exact dans trois bases de code différentes ces deux dernières années, et dans les trois l'auteur avait écrit une boucle for await...of propre, passé la revue de code et déployé en production. Le code semblait correct parce que la syntaxe était moderne. La fuite n'était dans aucune ligne ; elle était dans l'écart entre deux lignes — le .write() qui renvoyait false et l'itération suivante qui n'attendait jamais.
Theorem 3, appliqué à un booléen
Chez Everythink nous portons une affirmation formelle à travers the 21 papers, et c'est l'affirmation que cette fuite illustre le plus clairement : une propriété est garantie exactement quand son mécanisme est implémenté et mesuré. Le « et mesuré » est la moitié porteuse. Un mécanisme qui existe sur le papier mais n'est jamais observé dans la boucle est, aux fins de garantie, absent.
La backpressure est le cas canonique. Le mécanisme est présent dans le cœur de Node.js — .write() renvoie un booléen, drain se déclenche, writableNeedDrain bascule. Chaque pièce du protocole est implémentée. Ce qui manque, c'est la mesure : la branche qui lit le booléen et décide d'attendre. Sans cette branche, la propriété « mémoire bornée sous charge de streaming » n'est pas garantie — elle est simplement tolérée, jusqu'à ce que la charge dépasse ce que le conteneur peut tolérer.
C'est pourquoi nous nous méfions de toute affirmation de capacité qui nomme le mécanisme sans nommer la mesure. « Nous avons des streams » n'est pas une affirmation sur la mémoire bornée. « Nous vérifions le booléen et attendons drain sur chaque chemin d'écriture » l'est. Le HAI Engine ✅ tourne en production depuis 2016 sur exactement cette discipline : la couche de routage qui décide quelle Sister imagine, quel Oracle fusionne et dans quelle room atterrit le foresight est un mécanisme mesuré, pas un diagramme d'architecture. The space is the router — network → community → room — et le routage se produit avant que quoi que ce soit réponde, avec une discipline de backpressure à chaque saut.
Le multiplicateur de Node.js 22, et pourquoi les valeurs par défaut ne sont pas la sûreté
L'article de Master.dev note un changement qui a rendu la fuite silencieuse plus rapide : dans Node.js 22, le highWaterMark par défaut est passé de 16 Ko à 64 Ko (PR #52037 de Robert Nagy). Le changement est défendable — moins de commutations de contexte, meilleur débit sur les grosses charges — et c'est aussi un multiplicateur de 4x sur le tampon qui s'accumule avant que le premier signal de backpressure ne se déclenche. Dans un conteneur de 256 Mo ou 512 Mo, ce multiplicateur fait la différence entre une montée lente que le ramasse-miettes rattrape presque et une rapide qu'il ne rattrape pas.
[UNIQUE INSIGHT] Les valeurs par défaut qui améliorent le débit réallouent silencieusement une ressource finie — la mémoire de votre conteneur — sans vous prévenir. La même version qui rend le chemin heureux plus rapide fait planter le chemin malheureux plus tôt. Aucune note de version ne dit « votre seuil d'OOM est maintenant 4x plus proche », parce que le runtime n'a aucune idée de la limite de votre conteneur. L'opérateur, si. C'est un schéma général, pas une bizarrerie de Node.js : toute abstraction qui tamponne pour vous dépense une ressource qu'elle ne peut pas voir.
La remédiation de l'article est un instrument brutal — setDefaultHighWaterMark(false, 16 * 1024) pour réverter globalement — et un chirurgical — définir highWaterMark sur les streams qui comptent. Les deux sont corrects. Aucun n'est le point. Le point est que la valeur par défaut a bougé, presque personne n'a lu la note de version face au budget de son conteneur, et la fuite qui était toujours là a eu 4x plus de place pour grandir avant que quelqu'un ne la remarque. Les valeurs par défaut ne sont pas la sûreté. Les mécanismes mesurés sont la sûreté.
objectMode et le Transform à deux faces
Deux plis de l'article de Master.dev méritent d'être soulignés parce qu'ils brisent l'intuition restante des développeurs.
Premièrement, les streams en objectMode ne comptent pas les octets. Ils comptent les objets. Un highWaterMark de 16 signifie 16 objets tamponnés, et si chaque objet est une ligne JSON jointe de 50 Ko, l'étiquette « 16 » porte 800 Ko par flux avant le premier signal. Le chiffre sur le cadran n'est pas le chiffre dans votre tas.
Deuxièmement, un stream Transform a deux paramètres highWaterMark indépendants — un côté écriture, un côté lecture — et ils peuvent différer. Un Transform peut respecter parfaitement la backpressure côté lecture (en attendant que la réponse HTTP draine) tout en acceptant aveuglément les données côté écriture, parce que sa propre file d'objets interne n'a pas atteint la limite. L'article appelle cela « un accordéon qui se dilate pour absorber la pression, masquant le problème jusqu'à ce que ses propres tampons explosent ». Le correctif consiste à définir des limites asymétriques explicitement — un petit highWaterMark d'écriture pour pousser la backpressure en amont dès que le tampon aval se remplit.
C'est la même leçon que Theorem 3 enseigne sans cesse : un mécanisme à moitié câblé est à moitié absent. Un Transform qui respecte la backpressure d'un seul côté n'a que la moitié du protocole. La propriété — mémoire bornée de bout en bout — exige les deux moitiés, mesurées, sur chaque chemin que les données empruntent dans le système.
pipe() est de la syntaxe ; pipeline() est le mécanisme
La section de l'article sur .pipe() vs pipeline() est la déclaration la plus nette de la différence entre une abstraction fluide et un vrai mécanisme. .pipe() ne propage pas les erreurs. Si un transform au milieu d'une chaîne de pipe lève une exception, la source continue de lire, la destination reste ouverte, les descripteurs de fichier fuient, les sockets se bloquent et vous n'avez aucune indication que quelque chose s'est cassé. La syntaxe d'enchaînement — readStream.pipe(transformStream).pipe(writeStream) — se lit comme un pipeline Unix et cache une faille catastrophique derrière une belle syntaxe.
pipeline(), de node:stream/promises, détruit chaque stream de la chaîne à la moindre défaillance et propage l'erreur comme une promesse rejetée. C'est le standard depuis plus d'un demi-décennie. La règle de l'article est tranchante : si votre chaîne .pipe() a plus de deux streams, ou si un stream peut échouer, vous portez un risque que pipeline() élimine gratuitement.
Cela correspond à une habitude plus large que nous appliquons dans notre propre pile. Les invariants du dépôt sont explicites : la persistance est toujours atteinte via un port (trait), jamais un adaptateur Pg* concret ; les dépôts d'AppState sont des Arc<dyn Trait> pour que les tests injectent des mocks ; les Sisters n'écrivent jamais dans Postgres, elles renvoient SisterOutput et le Loom persiste. Chacune est une règle en forme de pipeline() — un mécanisme qui rend le mode de défaillance structurellement inaccessible, plutôt qu'une convention qui demande aux développeurs de se souvenir. Nous ne dépendons pas du développeur pour se souvenir de nettoyer les descripteurs de fichier. Nous faisons que le système de types impose que la seule façon de persister passe par un port qui gère le nettoyage.
Async/await cadence les lectures, pas les écritures
Le motif le plus dangereux de l'article, à mon avis, est celui qui paraît le plus moderne :
for await (const chunk of readable) {
writable.write(chunk);
}
L'itérateur asynchrone contrôle la vitesse à laquelle vous lisez. Il ne fait rien pour la vitesse à laquelle vous écrivez. Si writable.write() renvoie false, la boucle ne fait pas de pause — elle attrape le prochain morceau et le pousse dans un tampon déjà plein. Le correctif est les mêmes quatre lignes : vérifier le booléen, await once(writable, "drain") si faux. Ce seul await fait deux choses — il met la boucle en pause, ce qui met l'itérateur en pause, ce qui arrête le readable de tirer des données, et il cède l'exécution à la boucle d'événements, ce qui permet aux callbacks d'E/S de se déclencher et d'émettre drain. Sans ce yield, la boucle for monopolise le tick et drain ne pourrait jamais se déclencher.
La ligne de résumé de l'article est exacte : « Promises manage when your code runs. Backpressure manages how much data accumulates. async/await only solves one of them. » J'ajouterais le corollaire de Theorem 3 : une syntaxe moderne qui cadence la moitié du protocole est un mécanisme à moitié mesuré. L'autre moitié doit toujours être câblée à la main, et la syntaxe moderne rend plus facile d'oublier que le câblage manque.
Le coût caché de la pause : la famine de connexions
Le dernier mouvement de l'article est celui que la plupart des articles « backpressure résolue » omettent. Dès que vous respectez le booléen et attendez drain, la mémoire devient plate — et la pression remonte en amont dans votre pool de connexions à la base de données. Un stream Node.js en pause maintient son curseur de base de données ouvert. Si le client aval est sur un 3G instable et met cinq minutes à drainer, un worker de votre pool est immobilisé pendant cinq minutes. Un pool de 20 se sature sous 20 exports lents et volumineux. La mémoire est plate ; l'application cesse de servir de nouvelles requêtes ; les health checks échouent ; le répartiteur de charge déplace le trafic vers d'autres pods, qui atteignent leurs propres limites de pool. La défaillance en cascade n'a rien à voir avec la mémoire et tout à voir avec une ressource finie que vous avez oublié de protéger.
La remédiation que l'article propose est trois mouvements architecturaux, pas un changement de code : des délais de requête stricts, des pools de workers dédiés pour les exports lourds et une décharge par file d'attente vers le stockage d'objets avec une URL présignée. Le point est que corriger le mécanisme local (vérifier le booléen) expose le prochain mécanisme en amont (le pool) qui doit aussi être mesuré et borné. La backpressure n'est pas une propriété locale. C'est une chaîne, et chaque maillon a sa propre jauge.
C'est la discipline que nous appliquons chez Everythink à travers la pile. La passerelle World Monitor ✅ (Atlas) achemine chaque flux géo amont via un poller borné sur un calendrier fixe, avec un budget de token bucket par source, normalise en un GeoSignal et fait un upsert dans un cache Postgres durable. Les clients lisent le cache, jamais les upstreams — le volume d'appels amont est borné par notre calendrier, pas par le nombre de clients. C'est la même forme : une ressource finie (budget d'API amont) protégée par un mécanisme mesuré (le calendrier et le budget du poller), pas par l'espoir que les clients soient polis. Le chemin Sisters → Oracle ✅ a encore la même forme : la sortie de chaque imagine() de Sister est bornée par le protocole que le Loom fait respecter ; le merge() de l'Oracle normalise les probabilités à exactement un endroit pour que les consommateurs puissent compter sur sum(probability) ≈ 1.0. Mécanisme, mesuré, à un seul endroit.
[ORIGINAL DATA] Dans chaque post-mortem d'incident que nous avons examiné et qui impliquait un export par streaming, la cause racine n'était jamais « nous n'avions pas de streams ». C'était « la branche qui lit le booléen n'était pas sur le chemin d'écriture ». Le mécanisme était présent ; la mesure manquait. C'est tout l'écart entre un service qui tourne à 80 Mo et un qui monte à 3,8 Go et se fait tuer.
Points clés
- Un stream n'est pas une garantie de mémoire bornée. C'est un protocole coopératif avec un signal — le booléen de
.write()— que le consommateur doit lire. L'article de Master.dev remonte des OOM kills en production à du code qui ne l'a jamais lu. - Theorem 3 s'applique directement. Une propriété est garantie exactement quand son mécanisme est implémenté et mesuré. Le mécanisme de backpressure est implémenté dans le cœur de Node.js ; la mesure (la branche qui vérifie le booléen) est la partie que les équipes omettent. Sans la mesure, la propriété n'est pas garantie — elle est tolérée.
- Les valeurs par défaut bougent sans vous prévenir. Le quadruplement de
highWaterMarkdans Node.js 22 est un gain de débit et une fuite plus silencieuse et plus rapide. Les valeurs par défaut qui tamponnent pour vous dépensent une ressource (la mémoire de votre conteneur) qu'elles ne peuvent pas voir. - La moitié d'un protocole est à moitié absente. Un Transform qui respecte la backpression d'un seul côté, ou une boucle async qui cadence les lectures mais pas les écritures, est un mécanisme à moitié câblé. La propriété exige les deux moitiés, mesurées.
- Corriger la fuite locale expose la suivante. Respecter la backpressure remonte la pression en amont dans votre pool de connexions. La backpressure est une chaîne ; chaque maillon a besoin de sa propre jauge. Les trois remédiations de l'article (délais, pools dédiés, décharge par file) sont architecturales, pas syntaxiques.
Foire aux questions
Node.js ne gère-t-il pas la backpressure automatiquement quand j'utilise .pipe() ? Pour un pipe simple de deux streams, à peu près oui — .pipe() met le readable en pause quand le tampon du writable se remplit. Le point de l'article de Master.dev est que dès que vous ajoutez un transform, un socket réseau ou un chemin d'erreur, .pipe() cesse de propager les erreurs et commence à faire fuir des descripteurs de fichier. Utilisez pipeline() de node:stream/promises ; c'est le standard depuis plus d'un demi-décennie.
highWaterMark est-il une limite de mémoire ? Non. C'est un seuil consultatif. Quand le tampon l'atteint, .write() renvoie false. Pas d'exception, pas de pause automatique, pas de disjoncteur. Si vous ignorez le false, Node.js continue de tamponner jusqu'à ce que le processus meure. En objectMode, le nombre compte des objets, pas des octets — 16 peuvent représenter 800 Ko de JSON joint.
Si j'utilise for await...of, suis-je en sécurité ? Seulement côté lecture. L'itérateur asynchrone cadence la vitesse à laquelle vous tirez du readable. Il ne fait rien pour la vitesse à laquelle vous écrivez. Vous devez toujours vérifier la valeur de retour de .write() et await once(writable, "drain") quand elle vaut false. La ligne de l'article : « Promises manage when your code runs. Backpressure manages how much data accumulates. »
Quel rapport avec l'infrastructure de prévision ? La même discipline. Une propriété — mémoire bornée, probabilité calibrée, routage isolé — n'est garantie que lorsque son mécanisme est implémenté et mesuré. Chez Everythink, « the space is the router » signifie que la topologie network → community → room route avant que quoi que ce soit réponde, et chaque saut a sa propre jauge. Le HAI Engine tourne sur cette discipline depuis 2016.
La famine de connexions est-elle vraiment distincte de la fuite de mémoire ? Oui, et l'article est honnête sur le compromis. Respecter la backpressure aplatit la mémoire et déplace la pression dans votre pool de base de données. Un pool de 20 se sature sous 20 exports lents. Le correctif est architectural — délais, pools dédiés, décharge par file — pas un changement de code dans le stream.
Lisez the 21 papers — la série formalise Theorem 3 et la discipline du mécanisme mesuré que cette fuite illustre.
Sources
- Master.dev, « Your Node.js Streams Aren't Backpressuring. They're Silently Eating Your Memory. » (Durgesh Rajubhai Pawar, 25 mai 2026) — https://master.dev/blog/your-node-js-streams-arent-backpressuring-theyre-silently-eating-your-memory/

Le mécanisme doit correspondre au type de requête, pas l'assertion de récupération
L'expliqueur GraphRAG de ByteByteGo se lit comme cinq formes de mécanisme: similarité-pour-local, graphe-de-connaissance-pour-connexions, rapports-de-communauté-pour-global, map-reduce-pour-agrégation, routage-pour-type-de-requête. Theorem 3 appliqué à chacune.
→ →
La vérification à quatre couches est le mécanisme, pas l'assertion de fiabilité
Le guide de Ciberpatrulla sur la vérification pré-contractuelle d'entreprise se lit comme cinq formes de mécanisme: vérification-à-quatre-couches, source-publique-comme-mesure, architecture-en-couches-comme-routage, absence-comme-signal, cohérence-temporelle. Theorem 3 appliqué à chacune.
→ →
L'emplacement de l'état est le mécanisme, non l'étiquette d'agent
Lecture de l'Architecte Honnête de l'article de MachineLearningMastery sur la conception d'agents stateful vs stateless : six formes de mécanisme, Theorem 3 et parallèles transversaux aux Sisters stateless et au Loom stateful d'Everythink.
→ →Construisez votre monde sur un moteur qui prouve ce qu'il affirme.
Créez votre propre réseau sur le moteur qui tourne depuis 2016 — ou parlez à l'équipe derrière les 21 articles.
