Comment j'ai traqué un memory leak qui crashait mon Mac 5 fois en 2 heures
Mon MacBook 16 GB plantait en boucle. J'ai accusé les MCP servers, un plugin Claude Code, le navigateur Dia — avant de découvrir que le vrai coupable spawnait 2261 processus en 50 secondes.
MacBook Pro 16 GB crashe en boucle (5 fois en 2h). macOS force-kill des apps, swap explose à 15 GB, le système se fige. Erreur forkpty: Resource temporarily unavailable — plus aucun terminal ne s'ouvre.
@tailwindcss/postcss + Next.js 16 spawn un worker PostCSS par fichier CSS à recompiler, sans limite. Au premier hot-reload, cascade exponentielle : 0 → 283 → 651 → 2261 workers en 50 secondes. Chaque worker fait ~35 MB. Total : 77 GB de RAM demandée sur 16 GB physiques.
Désinstaller @tailwindcss/postcss (npm uninstall), supprimer postcss.config.mjs, retirer --turbopack du dev script. Next.js 16 gère Tailwind v4 nativement via @import 'tailwindcss' sans plugin PostCSS.
Cet article raconte une investigation de 2 heures avec 3 fausses pistes, un watchdog custom, et 5 crashes système avant d’identifier le vrai coupable.
Le symptôme
Mon Mac crash. Encore. Pour la cinquième fois aujourd’hui.
Pas un kernel panic propre — le genre de crash mou où macOS commence par tuer Safari, puis le Finder, puis se fige complètement. Le ventilateur hurle. Force reboot, ça repart 10 minutes, ça recrashe.
Suspect 1 : les MCP servers zombies
Premier check après le reboot :
ps aux -m | awk 'NR>1 {sum+=$6} END {printf "RAM: %.1f GB\n", sum/1024/1024}'# RAM: 18.2 GB
ps aux | grep 'claude' | grep -v grep | wc -l# 1018 GB de RSS sur une machine qui en a 16. Et 10 processus Claude alors que j’en ai lancé un seul. Le coupable semble évident.
En creusant, je découvre que le plugin claude-brain-sync spawne 6 subagents claude -p à chaque session, chacun chargeant l’intégralité des ~25 MCP servers configurés globalement. 50+ processus zombie. Je kill tout, je désinstalle le plugin. RAM : 18.2 → 12.9 GB.
Victoire.
Sauf que 20 minutes plus tard, ça recrashe.
Suspect 2 : les apps au démarrage
Je fais l’inventaire des login items — 12 apps au boot. SuperWhisper (1.66 GB), Dia (~1.1 GB), Beeper (435 MB). Ça fait 3.2 GB bouffés avant même d’ouvrir un terminal. Je nettoie : 7 apps retirées, 3 LaunchAgents orphelins supprimés (FigmaAgent sans Figma, CleanMyMac sans CleanMyMac, OpenClaw sans OpenClaw).
Mieux, mais ça recrashe quand même.
Suspect 3 : le navigateur Dia
Je construis un watchdog — un script bash qui log la RAM toutes les 5 secondes et dump un rapport complet quand ça dépasse un seuil. Premier spike capturé :
11:21:13 RAM:13.6GB ← tout va bien11:21:19 RAM:20.3GB ← +7 GB en 6 secondes11:21:24 RAM:25.6GB11:21:33 RAM:34.6GB11:21:41 RAM:42.8GB11:21:49 RAM:51.9GB11:21:57 RAM:62.4GB11:22:05 RAM:72.1GB ← 72 GB en 50 secondesDe 13.6 à 72 GB en moins d’une minute. Le snapshot montre ~25 processus Dia (navigateur). Je pointe Dia du doigt.
Mais quelque chose cloche : le snapshot ne montre que ~1.5 GB de processus identifiés pour 72 GB reportés. Et Dia tourne sans problème le reste du temps.
Le signal qui a tout changé
À ce stade, j’ai un watchdog qui log toutes les 5 secondes mais qui ne peut pas empêcher le crash. Au spike suivant, macOS s’effondre tellement vite que plus aucun terminal ne s’ouvre :
[forkpty: Resource temporarily unavailable][Could not create a new process and open a pseudo-tty.]Plus de PTY disponibles. Le système a épuisé ses pseudo-terminaux — soit parce qu’il n’a plus de mémoire pour en allouer, soit parce que des centaines de processus les ont tous réservés. Je ne peux plus rien inspecter, plus rien kill. Force reboot.
C’est ce message qui m’a poussé à chercher plus loin. Le problème n’est pas une app qui consomme trop de RAM — c’est quelque chose qui spawne des centaines de processus. Assez pour épuiser les PTY du système en quelques secondes. J’améliore le watchdog pour compter et nommer les processus par application, et j’ajoute un auto-kill au-delà de 20 GB.
Le watchdog v2 : les noms de processus
J’améliore le watchdog pour agréger la RAM par application. Au prochain spike, les logs montrent la vérité :
11:41:11 PostCSS:0 RAM:14.4GB ← normal11:41:17 PostCSS:283 RAM:18.5GB ← 283 workers apparaissent11:41:23 PostCSS:561 RAM:26.9GB ← doublent en 6 secondes11:41:31 PostCSS:884 RAM:36.5GB11:41:40 PostCSS:1183 RAM:46.8GB11:41:48 PostCSS:1531 RAM:57.2GB11:42:05 PostCSS:2261 RAM:79.4GB ← 2261 workersLe coupable n’était ni les MCP, ni Dia, ni les login items. C’était @tailwindcss/postcss dans un projet Next.js 16 qui spawnait des milliers de workers sans limite.
Le mécanisme de la bombe
flowchart TD Dev["npm run dev<br/>(Next.js 16)"] --> Server["next-server<br/>~300 MB"] Server -->|"hot-reload<br/>fichier modifié"| PostCSS["@tailwindcss/postcss"] PostCSS -->|"1 worker par import CSS"| W1["postcss.js worker #1<br/>~35 MB"] PostCSS --> W2["postcss.js worker #2<br/>~35 MB"] PostCSS --> W3["postcss.js worker #3<br/>~35 MB"] PostCSS --> WN["... worker #2261<br/>~35 MB"] W1 -.->|"jamais tué"| LEAK["Workers s'accumulent<br/>pas de limite, pas de pool"] W2 -.-> LEAK W3 -.-> LEAK WN -.-> LEAK LEAK --> OOM["79 GB demandé<br/>sur 16 GB physiques<br/>→ swap spiral → crash"] style PostCSS fill:#FF5E78,color:#0E0E10 style LEAK fill:#FF5E78,color:#0E0E10 style OOM fill:#FF5E78,color:#0E0E10 style W1 fill:#FF5E78,color:#0E0E10 style W2 fill:#FF5E78,color:#0E0E10 style W3 fill:#FF5E78,color:#0E0E10 style WN fill:#FF5E78,color:#0E0E10
@tailwindcss/postcss spawne un worker Node.js par fichier CSS résolu lors d’une recompilation. Next.js 16 en dev recompile agressivement au moindre changement. Chaque worker fait ~35 MB. Les workers ne sont pas poolés ni limités — ils s’empilent.
Le résultat en timeline :
gantt title RAM (GB) — 3 spikes en 2 heures dateFormat HH:mm axisFormat %H:%M section Spike 1 (PostCSS) 17→35 GB, crash :crit, 11:12, 2m macOS kill + reboot :done, 11:14, 2m section Spike 2 (PostCSS + Dia) 13→72 GB en 50s :crit, 11:21, 1m crash système :crit, 11:22, 1m section Spike 3 (watchdog v3) 10→31 GB :crit, 11:48, 1m auto-kill sauve :done, 11:49, 1m
Le fix
Trois lignes :
# 1. Désinstaller le plugin PostCSS (le vrai coupable)npm uninstall @tailwindcss/postcss
# 2. Supprimer le fichier de config PostCSSrm postcss.config.mjs
# 3. Retirer Turbopack du dev script (aggravait le bug)# package.json: "dev": "next dev" (au lieu de "next dev --turbopack"){
"scripts": {
"dev": "next dev --turbopack"
},
"dependencies": {
"@tailwindcss/postcss": "^4.3.0",
"tailwindcss": "^4.3.0",
"next": "^16.2.6"
}
}{
"scripts": {
"dev": "next dev --turbopack"
},
"dependencies": {
"@tailwindcss/postcss": "^4.3.0",
"tailwindcss": "^4.3.0",
"next": "^16.2.6"
}
}{
"scripts": {
"dev": "next dev"
},
"dependencies": {
"tailwindcss": "^4.3.0",
"next": "^16.2.6"
}
}{
"scripts": {
"dev": "next dev"
},
"dependencies": {
"tailwindcss": "^4.3.0",
"next": "^16.2.6"
}
}Pourquoi ça marche sans le plugin ? Tailwind v4 s’active via @import "tailwindcss" dans le CSS. Next.js 16 sait résoudre cet import nativement — le plugin PostCSS est redondant et c’est lui qui crée les workers.
Résultat après le fix :
11:51:21 PostCSS:1 NextServer(1x567MB) ← dev server lancé11:51:32 PostCSS:0 NextServer(1x523MB) ← compilation terminée11:52:47 PostCSS:0 NextServer(1x498MB) ← stable, 9.3 GB total1 worker PostCSS temporaire au lieu de 2261. Pas de fuite.
Le watchdog
Le script qui a permis de résoudre ça : un bash de 120 lignes qui tourne dans un terminal.
# ~/bin/claude-watchdog.sh — toutes les 5 secondes :# 1. Log RAM, swap, Claude count, MCP count, PostCSS count# 2. Top 5 apps par RAM (agrégé par nom)# 3. Si > 25 GB → dump un rapport complet (RAM par app, memory pressure, swap)# 4. Si > 20 GB → auto-kill des PostCSS workers + next dev serversLa timeline qu’il produit est ce qui a permis de voir que le spike passait de 0 à 283 PostCSS workers en 6 secondes — invisible sans monitoring continu.
Bonus : une app menu bar pour ne plus jamais se faire avoir
Le watchdog bash fait le job mais il faut un terminal dédié. J’ai fini par écrire une app macOS native — Claude Monitor — qui vit dans la menu bar et surveille en permanence.
C’est une app SwiftUI de quelques centaines de lignes, compilée avec Swift Package Manager, zéro dépendance. Le singe 🐵 dans la menu bar affiche en temps réel le nombre de Claude et de serveurs MCP. Un clic ouvre le dashboard :
- RAM en gros avec barre de progression colorée (vert/orange/rouge)
- Espace disque disponible avec barre used/total (le chiffre du Finder)
- Compteurs : instances Claude, serveurs MCP, et le nombre de process de ton user rapporté à la limite
kern.maxprocperuid(~2666 ici) — exactement le plafond que la bombe PostCSS faisait exploser, celui qui déclenche leforkpty: Resource temporarily unavailable - Top apps par consommation mémoire avec mini barres visuelles
- Bouton kill : un clic pour tuer tous les serveurs MCP
- Auto-kill configurable : sliders pour le seuil (% de la limite de process + GB de RAM)

L’auto-kill vérifie toutes les 5 secondes. Si le nombre de process approche la limite ou que la RAM explose, il reape le swarm de serveurs MCP avant que le Mac ne se fige. C’est le watchdog bash, mais en app native, toujours visible, toujours actif.
Le code est dans my-monkeys/claude-monitor — un swift build -c release et c’est installé.
Ce que j’ai appris
1. Le premier suspect est rarement le bon. J’ai accusé les MCP servers, un plugin Claude Code, les login items, et un navigateur avant de trouver le vrai coupable. Chacun avait l’air coupable sur le moment.
2. Sans monitoring, tu debuggues à l’aveugle. Les 4 premiers crashes m’ont donné zéro information utile — macOS kill les processus avant que tu puisses les inspecter. Le watchdog a tout changé : 5 secondes entre chaque snapshot, agrégation par app, timeline lisible.
3. ps aux ne montre pas tout. Le RSS reporté par ps ne compte pas la mémoire compressée, les pages mappées kernel-side, ni le swap inflight. Un process à “35 MB” peut consommer bien plus en réalité. vm_stat et sysctl vm.swapusage racontent une histoire différente.
4. Les cascades sont silencieuses. 2261 processus en 50 secondes, et le seul symptôme visible c’est “le Mac rame”. Pas d’alerte, pas de log, pas de notification. macOS n’a aucun mécanisme pour dire “hey, un process vient de forker 500 enfants en 10 secondes, c’est normal ?”
Chargement…