Debian 13 « Broken pipe » : quand c’est sans danger et quand c’est votre premier avertissement (cas n°75)

Cet article vous a aidé ?

Vous surveillez un déploiement sur Debian 13. Tout a l’air normal — la latence est stable, les budgets d’erreur calmes — puis les logs se mettent à déferler :
broken pipe. Certaines équipes le traitent comme du lint. D’autres comme une panne. Les deux ont parfois raison, et souvent tort.

« Broken pipe » est l’une de ces erreurs qui ne vous dit pas ce qui a échoué. Elle vous dit quand vous avez appris l’échec :
vous avez essayé d’écrire sur une connexion dont l’autre côté avait déjà fermé. La vraie question est pourquoi il a fermé — et si c’était normal.

Ce que signifie vraiment « broken pipe » sur Debian 13

Sur Debian 13, comme sur tout autre Linux moderne, « broken pipe » est généralement votre application exposant une condition UNIX classique :
EPIPE (errno 32). En termes simples : votre processus a tenté un write() (ou un envoi équivalent)
vers un pipe ou un socket qui n’a plus de lecteur à l’autre extrémité.

De où vient le message

  • Les applications affichent « Broken pipe » quand elles interceptent EPIPE et le loggent (Go, Java, Python, Ruby, Nginx, Apache, clients PostgreSQL, etc.).
  • Les pipelines shell l’affichent parfois lorsqu’un consommateur en aval sort prématurément (penser à head), et que l’amont continue d’écrire.
  • Les bibliothèques peuvent convertir EPIPE en exception (p. ex. java.io.IOException: Broken pipe, BrokenPipeError: [Errno 32] en Python).
  • Les signaux : par défaut, écrire sur un pipe fermé peut déclencher SIGPIPE qui termine le processus sauf si ce signal est géré/ignoré. Beaucoup de serveurs réseau ignorent SIGPIPE pour éviter de mourir et enregistrent plutôt EPIPE.

Le détail opérationnel clé : EPIPE est la découverte par l’écrivain que le pair a disparu. Cela ne prouve pas que l’écrivain a un bug,
et cela ne prouve pas que le réseau est mauvais. Cela prouve une inadéquation du cycle de vie : un côté écrit encore, l’autre a fini.

Broken pipe vs. connection reset : pourquoi la distinction compte

Les ingénieurs confondent souvent « broken pipe » et « connection reset by peer ». Ils sont liés mais pas identiques.
« Connection reset » est typiquement ECONNRESET : le pair (ou un middlebox) a envoyé un RST TCP, détruisant la connexion brutalement.
« Broken pipe » est généralement EPIPE quand vous écrivez après que le pair a fermé (FIN) et que vous l’avez traité.

Cette nuance importe parce qu’un FIN après réponse est normal pour beaucoup de clients, alors qu’une pluie de RST peut signifier un timeout de proxy, une pression du noyau,
ou un processus de service abattu. Debian 13 n’a pas changé TCP, mais de nouveaux paramètres par défaut et des versions plus récentes de démons peuvent changer
la fréquence à laquelle vous voyez ces messages.

Une citation que je garde en tête : Werner Vogels (CTO d’Amazon) a popularisé l’état d’esprit fiabilité —
« Everything fails, all the time » (idée paraphrasée). « Broken pipe » est souvent votre premier petit postcard de cette réalité.

Bruit inoffensif vs premier avertissement

Quand « broken pipe » est généralement inoffensif

Vous pouvez diminuer l’urgence quand toutes ces conditions sont vraies :

  • Il corrèle avec des abandons côté client : utilisateurs quittant la page, clients mobiles changeant de réseau, onglets de navigateur fermés.
  • Les erreurs sont en faible proportion : un faible pourcentage de requêtes, stable dans le temps, sans tendance à la hausse sous charge.
  • Ça arrive après que les en-têtes/corps de réponse ont surtout été envoyés : classique « client a fermé la connexion pendant l’envoi de la réponse ».
  • C’est sur des endpoints avec gros téléchargements : fichiers volumineux, réponses en streaming, SSE/websocket-like sans keepalive adapté.
  • Les métriques ne montrent pas d’impact : p95/p99 latences, taux 5xx, saturation et profondeurs de file d’attente sont calmes.

Exemple : Nginx loggue broken pipe parce que le navigateur a arrêté de lire une grande réponse. Votre serveur n’a rien fait de mal ;
votre niveau de log est juste honnête.

Quand « broken pipe » est votre premier avertissement

Traitez-le comme une odeur de pager quand l’une de ces conditions est vraie :

  • Pics de taux pendant des déploiements : suggère churn de connexions, démarrages lents, problèmes de readiness, ou anciens pods drainant mal.
  • Il coïncide avec des timeouts : timeouts upstream, timeouts de proxy, timeouts d’équilibreur de charge.
  • Il apparaît avec pression mémoire/CPU : le serveur se bloque, les clients abandonnent, puis les écritures atteignent des sockets morts.
  • Il se concentre sur des pairs spécifiques : une AZ, une gateway NAT, une couche de proxy, un endpoint lié au stockage.
  • Il apparaît dans des RPC internes : les appels service-à-service qui se cassent sont rarement du « comportement utilisateur ».
  • Il corrèle avec retransmissions/RST TCP : suggère perte de paquets, problèmes MTU, conntrack saturé, ou souci de middlebox.

Blague n°1 : « Broken pipe » est l’équivalent logistique de votre collègue disant « il faut qu’on parle » — ça peut ne rien être, mais vous ne dormirez pas tant que vous ne saurez pas.

La règle opérationnelle

Si « broken pipe » concerne surtout le trafic edge et que la santé du service est verte, c’est du bruit que vous pouvez dompter.
S’il concerne le trafic est-ouest, augmente, ou s’il est accompagné de timeouts/5xx, c’est un symptôme. Cherchez la cause sous-jacente, pas la chaîne de caractères.

Faits et historique à utiliser dans un postmortem

  1. « Broken pipe » vient des pipes UNIX : écrire dans un pipe sans lecteur levait historiquement SIGPIPE ; l’expression précède les services TCP modernes.
  2. EPIPE est errno 32 sur Linux : c’est pourquoi vous voyez « Errno 32 » en Python et autres.
  3. L’action par défaut de SIGPIPE est la terminaison du processus : beaucoup de serveurs ignorent explicitement SIGPIPE pour survivre aux déconnexions client.
  4. TCP a deux modes communs de terminaison : une fermeture propre (FIN) vs. un reset abrupt (RST). Signatures d’échec différentes, responsabilités différentes.
  5. HTTP/1.1 keep-alive a rendu cela plus visible : les connexions persistantes ont augmenté la surface pour les timeouts inactifs et les états half-closed.
  6. Les reverse proxies loguent beaucoup « broken pipe » : ils se trouvent entre clients et upstreams et sont les premiers à remarquer quand un côté se barre.
  7. Les middleboxes adorent les timeouts : load balancers, gateways NAT, firewalls et proxies appliquent souvent des timers d’inactivité que les applications oublient d’aligner.
  8. Linux peut rapporter des erreurs de socket tard : un write peut réussir localement puis rapporter l’échec plus tard ; c’est pourquoi le timing semble « aléatoire ».
  9. « Client aborted » n’est pas toujours un utilisateur : checks de santé, monitoring synthétique, scanners et SDK agressifs ouvrent-et-coupent aussi des connexions.

Guide de diagnostic rapide

C’est l’ordre qui trouve les véritables goulots rapidement. Optimisé pour la réalité de l’on-call : vous avez besoin d’une direction en 10 minutes, pas d’une thèse en 10 heures.

Premier : classifier où ça arrive (edge vs interne)

  • Edge / HTTP public : commencez par les logs de proxy (Nginx/Apache/Envoy), les modèles d’abandon client et les timeouts.
  • RPC interne / base de données : traitez-le comme un incident de fiabilité tant que le contraire n’est pas prouvé.
  • Scripts shell / cron : souvent un comportement inoffensif de pipeline, mais peut masquer des problèmes de sortie partielle.

Deuxième : corréler avec timeouts et saturation

  • Vérifiez 499/408/504 (ou équivalents) et compteurs de timeouts upstream.
  • Vérifiez CPU steal, file d’exécution, pression mémoire, IO wait, latence disque.
  • Vérifiez retransmissions/resets réseau.

Troisième : confirmer qui a fermé en premier

  • Capture de paquets d’une minute sur le nœud affecté (oui, en 2025 tcpdump paye toujours son loyer).
  • Inspectez les réglages keepalive sur client, proxy, load balancer et serveur.
  • Cherchez le comportement de deploy/drain : readiness gates, connection draining, arrêt gracieux.

Quatrième : décider de la classe de correctif

  • Bruit : ajuster la journalisation, réduire les traces de pile, ajouter de l’échantillonnage, et améliorer la visibilité des abandons clients.
  • Mauvais alignement de timeouts : aligner idle timeouts et keepalives ; ajuster le buffering du proxy.
  • Surcharge : ajouter de la capacité, réduire le travail par requête, corriger les IO lents (stockage ou réseau), ajouter du backpressure.
  • Crashs : corriger OOM, segfault, redémarrages en chaîne, et mauvaises pratiques de déploiement.

Tâches pratiques : commandes, sorties, décisions (12+)

Ce sont les tâches que j’exécute réellement sur des hôtes Debian. Chacune a trois parties : la commande, ce que signifie la sortie, et la décision suivante.
Les commandes supposent root ou sudo quand nécessaire.

Task 1: Find the exact error wording and which service emits it

cr0x@server:~$ sudo journalctl -S -2h -p warning..alert | grep -i -E "broken pipe|EPIPE|SIGPIPE" | tail -n 20
Dec 31 09:12:41 api-01 nginx[1234]: *918 writev() failed (32: Broken pipe) while sending to client, client: 203.0.113.10, server: _, request: "GET /download.bin HTTP/1.1"
Dec 31 09:13:02 api-01 app[8871]: ERROR send failed: Broken pipe (os error 32)

Signification : Vous savez maintenant si c’est Nginx, l’appli, un sidecar, ou autre.
Nginx « while sending to client » hurle abandon client ; les échecs d’envoi au niveau app peuvent concerner l’aval (BD, cache, RPC interne).
Décision : Séparez la voie d’investigation : modèles de trafic edge pour Nginx ; traçage de dépendances pour les erreurs applicatives.

Task 2: Check if deploys or restarts line up with the spike

cr0x@server:~$ sudo journalctl -S -6h -u nginx -u app.service --no-pager | grep -E "Started|Stopping|Reloaded|SIGTERM|exited" | tail -n 30
Dec 31 08:59:58 api-01 systemd[1]: Reloaded nginx.service - A high performance web server and a reverse proxy server.
Dec 31 09:00:01 api-01 systemd[1]: Stopping app.service - API Service...
Dec 31 09:00:03 api-01 systemd[1]: app.service: Main process exited, code=killed, status=15/TERM
Dec 31 09:00:03 api-01 systemd[1]: Started app.service - API Service.

Signification : Le churn de connexions pendant un reload/restart peut produire EPIPE quand des clients en vol perdent leur upstream.
Décision : Si les erreurs se concentrent autour des redémarrages, inspectez l’arrêt gracieux, le drainage des connexions, la readiness et les retries du proxy.

Task 3: Look for client-abort status codes at the proxy

cr0x@server:~$ sudo awk '$9 ~ /^(499|408|504)$/ {c[$9]++} END{for (k in c) print k, c[k]}' /var/log/nginx/access.log
499 317
504 12
408 41

Signification : Beaucoup de 499 signifient généralement que le client a fermé tôt (convention Nginx). 504 suggère timeout upstream.
408 indique timeout de requête (client trop lent ou lecture d’en-têtes expirée).
Décision : Si 499 domine avec des 5xx stables, c’est probablement du bruit inoffensif ; si 504 augmente avec EPIPE, poursuivez sur latence/surcharge upstream.

Task 4: Identify which endpoint is generating the broken pipes

cr0x@server:~$ sudo awk '$9==499 {print $7}' /var/log/nginx/access.log | sort | uniq -c | sort -nr | head
  211 /download.bin
   63 /reports/export
   31 /api/v1/stream
   12 /favicon.ico

Signification : Les téléchargements et exports de longue durée attirent classiquement les abandons clients.
Décision : Si c’est concentré sur de grosses réponses, envisagez buffering, support de ranges, téléchargements reprenables, et échantillonnage des logs pour 499/EPIPE.

Task 5: Check upstream response time patterns (is the server slow?)

cr0x@server:~$ sudo awk '{print $(NF-1)}' /var/log/nginx/access.log | awk -F= '{print $2}' | sort -n | tail -n 5
12.991
13.105
13.442
14.003
15.877

Signification : Ceci suppose que vous logguez quelque chose comme upstream_response_time=....
De fortes latences en queue peuvent pousser les clients à expirer ou abandonner, provoquant EPIPE quand vous écrivez enfin.
Décision : Si les queues sont élevées, pivotez vers vérifications CPU, mémoire et IO ; vérifiez aussi que les timeouts du proxy ne sont pas trop agressifs.

Task 6: Confirm system pressure (CPU, load, memory) around the event

cr0x@server:~$ uptime
 09:14:22 up 23 days,  4:11,  2 users,  load average: 18.91, 19.22, 17.80

Signification : Une charge moyenne bien au-dessus du nombre de CPU (ou au-dessus de la normale pour cet hôte) suggère contention.
Décision : Si la charge est élevée, validez s’il s’agit de saturation CPU, de IO wait ou d’un backlog de processus exécutables.

cr0x@server:~$ free -h
               total        used        free      shared  buff/cache   available
Mem:            31Gi        29Gi       420Mi       1.2Gi       1.6Gi       1.0Gi
Swap:          2.0Gi       1.8Gi       200Mi

Signification : Peu de mémoire disponible plus swap élevé est la façon d’obtenir « tout est lent, les clients abandonnent, les écritures EPIPE ».
Décision : Si swapping, traitez broken pipe comme symptôme. Stoppez la fuite mémoire, ajoutez de la mémoire, réduisez la concurrence, ou corrigez la stratégie de cache.

Task 7: Check for OOM kills (the silent broken-pipe factory)

cr0x@server:~$ sudo journalctl -k -S -6h | grep -i -E "oom|killed process|out of memory" | tail -n 20
Dec 31 09:00:02 api-01 kernel: Out of memory: Killed process 8871 (app) total-vm:4123456kB, anon-rss:1987654kB, file-rss:0kB, shmem-rss:0kB

Signification : Si le noyau tue votre appli, les clients verront des déconnexions ; les upstreams verront broken pipe en essayant d’écrire.
Décision : Ne traitez plus EPIPE comme une simple nuisance de log ; corrigez les limites mémoire, les fuites ou les fan-out de requêtes provoquant des pics.

Task 8: Inspect TCP resets and retransmits (network truth serum)

cr0x@server:~$ sudo nstat -az | egrep "TcpExtTCPSynRetrans|TcpRetransSegs|TcpOutRsts|TcpInRsts"
TcpExtTCPSynRetrans             18                 0.0
TcpRetransSegs                  4201               0.0
TcpOutRsts                      991                0.0
TcpInRsts                       874                0.0

Signification : Retransmissions et RST en hausse peuvent indiquer perte de paquets, surcharge, problèmes conntrack, ou enforcement de timeouts inactifs.
Décision : Si RSTs/retransmits augmentent avec EPIPE, capturez le trafic et examinez les timeouts des middleboxes ; vérifiez aussi les erreurs NIC.

Task 9: Check NIC and kernel-level drops/errors

cr0x@server:~$ ip -s link show dev eth0
2: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc fq_codel state UP mode DEFAULT group default qlen 1000
    RX:  bytes packets errors dropped  missed   mcast
    9876543210 1234567      0   12451       0   12345
    TX:  bytes packets errors dropped carrier collsns
    8765432109 2345678      0    9421       0       0

Signification : Les drops au niveau de l’interface ne sont pas toujours fatals, mais corrèlent fortement avec retransmits et déconnexions « aléatoires ».
Décision : Si les drops augmentent, vérifiez le qdisc/backlog, la modération d’interruption, la saturation CPU de l’hôte, ou la congestion réseau en amont.

Task 10: See which connections are in which state (are you drowning in half-closed sockets?)

cr0x@server:~$ ss -s
Total: 2134
TCP:   1821 (estab 712, closed 943, orphaned 7, timewait 122, ports 0)
Transport Total     IP        IPv6
RAW       0         0         0
UDP       31        27        4
TCP       878       831       47
INET      909       858       51
FRAG      0         0         0

Signification : Beaucoup de timewait peut être normal pour des connexions courtes, mais de grandes variations peuvent indiquer des problèmes keepalive ou de réutilisation.
Décision : Si estab est énorme et stable, vous avez peut-être des clients lents ; si closed/orphaned monte, investiguez des terminaisons abruptes et des crashs.

cr0x@server:~$ ss -tan state time-wait '( sport = :443 )' | head
State      Recv-Q Send-Q Local Address:Port Peer Address:Port
TIME-WAIT  0      0      10.0.1.10:443     198.51.100.25:51322
TIME-WAIT  0      0      10.0.1.10:443     203.0.113.77:44118

Signification : Confirme la répartition par port et état.
Décision : Si TIME-WAIT est excessif et cause épuisement de ports chez les clients ou NAT, augmentez la réutilisation via le design (keepalive) plutôt que des astuces noyau.

Task 11: Verify timeout mismatches (Nginx example)

cr0x@server:~$ sudo nginx -T 2>/dev/null | egrep "keepalive_timeout|proxy_read_timeout|proxy_send_timeout|send_timeout" | head -n 20
keepalive_timeout 65;
send_timeout 30s;
proxy_read_timeout 60s;
proxy_send_timeout 60s;

Signification : Si send_timeout est plus court que votre temps de réponse typique pour de gros téléchargements, vous verrez des broken pipes.
Si proxy_read_timeout est trop bas, les requêtes upstream seront coupées, et le proxy peut resetter.
Décision : Alignez les timeouts sur toute la chaîne (client ↔ LB ↔ proxy ↔ app). Augmentez les timeouts seulement si vous pouvez accepter le blocage de ressources.

Task 12: Inspect systemd service limits and kill behavior

cr0x@server:~$ sudo systemctl show app.service -p TimeoutStopUSec -p KillSignal -p KillMode -p Restart -p RestartSec
TimeoutStopUSec=30s
KillSignal=SIGTERM
KillMode=control-group
Restart=on-failure
RestartSec=2s

Signification : Si votre app a besoin de 90 secondes pour drainer, mais systemd lui accorde 30, vous générerez des déconnexions pendant les déploiements.
Décision : Ajustez l’arrêt gracieux : augmentez TimeoutStopUSec, implémentez des endpoints de drainage, et assurez-vous que le proxy cesse d’envoyer du trafic avant l’arrêt.

Task 13: Confirm who closed first with a short tcpdump

cr0x@server:~$ sudo tcpdump -i eth0 -nn -s 0 -c 50 'tcp port 443 and (tcp[tcpflags] & (tcp-fin|tcp-rst) != 0)'
tcpdump: verbose output suppressed, use -v[v]... for full protocol decode
listening on eth0, link-type EN10MB (Ethernet), snapshot length 262144 bytes
09:13:02.112233 IP 203.0.113.10.51544 > 10.0.1.10.443: Flags [F.], seq 12345, ack 67890, win 64240, length 0
09:13:02.112450 IP 10.0.1.10.443 > 203.0.113.10.51544: Flags [R], seq 67890, win 0, length 0

Signification : Vous voyez un FIN du client puis un reset du serveur (ou l’inverse). C’est la vérité terrain sur qui a raccroché.
Décision : Si les clients envoient des FIN tôt, c’est probablement des abandons/timeouts côté client ; si le serveur envoie des RST, cherchez des fermetures du proxy/app, crashs, ou timeouts agressifs.

Task 14: If storage is involved, check IO latency (slow disk makes clients bail)

cr0x@server:~$ iostat -xz 1 3
Linux 6.12.0 (api-01)  12/31/2025  _x86_64_  (8 CPU)

avg-cpu:  %user   %nice %system %iowait  %steal   %idle
          22.10    0.00    5.20   18.30    0.00   54.40

Device            r/s     w/s   rkB/s   wkB/s  avgrq-sz avgqu-sz   await r_await w_await  svctm  %util
nvme0n1         90.0   120.0  8200.0  5400.0     92.0     7.80   38.20   41.10   35.90   0.80  17.00

Signification : Un await élevé et un %iowait important peuvent bloquer le traitement des requêtes. Les clients expirent, puis vous écrivez : EPIPE.
Décision : Si la latence disque augmente avec les broken pipes, corrigez le chemin IO : optimisation de requêtes, cache, stockage plus rapide, ou backpressure — pas seulement des timeouts.

Trois mini-récits d’entreprise depuis le terrain

1) Incident causé par une mauvaise hypothèse : « Broken pipe, c’est toujours un abandon client »

Une entreprise SaaS de taille moyenne a mis à jour une flotte Debian et a remarqué plus de lignes « broken pipe » dans ses logs API. Le lead on-call a balayé d’un revers :
« Ce sont des gens qui ferment les onglets. » C’était plausible ; leur trafic venait surtout de navigateurs, et les graphiques semblaient corrects — au début.

Deux jours plus tard, une intégration partenaire a commencé à échouer. Pas des navigateurs. Des machines. Le job batch du partenaire ouvrait une connexion, postait une charge,
puis attendait une réponse. Son job réessayait en cas d’échec, augmentant la concurrence. Pendant ce temps, le serveur API était lent parce qu’un job de compactage en arrière-plan saturait le disque.
Les requêtes faisaient la queue. Le partenaire atteignait son timeout côté client et fermait les sockets.

L’API a fini par écrire des réponses sur des connexions qui n’existaient plus. EPIPE a inondé les logs. L’erreur n’était pas la maladie ;
c’était un symptôme de latence suffisante pour déclencher les timeouts client. Quand les retries ont augmenté, la file a empiré, et les graphiques
n’étaient plus corrects. Ils étaient en feu.

La correction n’était pas « supprimer les logs broken pipe ». Il fallait prioriser l’IO foreground, déplacer le compactage hors pic, ajouter du rate limiting,
et aligner les timeouts client sur les SLO du service. Après ça, les broken pipes sont retombés à un niveau bas et ennuyeux — exactement là où ils doivent être.

2) Optimisation qui s’est retournée contre eux : réglages keepalive agressifs

Dans une grande entreprise avec plus de proxies que d’employés (à peu près), quelqu’un a voulu « optimiser » la gestion des connexions.
L’idée : garder les connexions ouvertes plus longtemps pour réduire les handshakes TLS et la CPU. Ils ont donc augmenté les timeouts keepalive sur le proxy inverse et l’appli.

Le load balancer en amont n’a pas reçu la note. Il avait un timeout d’inactivité plus court. Le LB a donc supprimé en silence les connexions inactives, et le proxy inverse
a essayé de les réutiliser joyeusement. De temps en temps, une connexion réutilisée était déjà morte. L’écriture suivante déclenchait un reset ou un broken pipe,
selon qui s’en apercevait en premier.

Pire : le keepalive prolongé signifiait plus de sockets inactifs. L’utilisation des descripteurs de fichiers a augmenté. Lors de pics de trafic, le proxy a atteint sa limite de fd,
a commencé à refuser des accept, et les clients ont retry. Vous aviez maintenant à la fois churn inutile de reconnexions et épuisement de ressources. Maîtrise du mauvais goulet.

La solution finale était ennuyeuse et correcte : aligner les timeouts d’inactivité LB ↔ proxy ↔ app, limiter le nombre de requêtes keepalive, et fixer des limites fd sensées.
La CPU a un peu augmenté. Le taux d’incidents a beaucoup baissé. C’est un compromis acceptable.

3) Pratique ennuyeuse mais efficace qui a sauvé la mise : arrêt gracieux et discipline de drainage

Une plateforme du secteur financier utilisait une stack basée sur Debian derrière une couche de proxy. Ils avaient une culture du « pas de héros » : chaque service avait
un comportement d’arrêt documenté et un processus de drainage appliqué. Ça paraissait bureaucratique jusqu’au moment où ça ne l’était plus.

Un après-midi, une mise à jour du noyau a exigé des reboots. Les reboots sont le terrain de prédilection des erreurs « broken pipe » : connexions coupées en vol,
clients qui retry, upstreams qui cafouillent. Mais cette fois, l’équipe a suivi sa checklist. Les nœuds ont été cordonnés (ou retirés du routage),
le trafic drainé, les longues requêtes autorisées à se terminer, puis les services arrêtés.

Dans les logs, « broken pipe » a à peine clignoté. Plus important : les taux d’erreur côté client sont restés stables. L’équipe a pu rentrer à l’heure.
Le seul drame a été la discussion sur le fait que le processus était « trop lent », ce que les gens disent toujours juste avant que la manière rapide ne leur coûte le week-end.

La leçon : drainer les connexions et respecter les fenêtres d’arrêt gracieux évite une quantité surprenante de bruit EPIPE et de douleur utilisateur réelle.
Ce n’est pas glamour. Ça fonctionne.

Blague n°2 : le « déploiement rapide » qui ignore le drainage, c’est comme speedrunner une usine de porcelaine — techniquement impressionnant, financièrement déroutant.

Erreurs courantes : symptôme → cause → correction

1) Symptom: Nginx logs “writev() failed (32: Broken pipe) while sending to client”

Cause racine : Le client s’est déconnecté en cours de réponse (onglet fermé, changement de réseau mobile, SDK impatient).
Parfois déclenché par un serveur lent.

Correction : Si c’est le baseline normal, réduisez le niveau de log/échantillonnage pour cette classe ; si le pic corrèle avec la latence, corrigez la lenteur d’abord.

2) Symptom: App logs EPIPE on writes to upstream (Redis/DB/internal HTTP)

Cause racine : L’upstream a fermé la connexion à cause d’un timeout d’inactivité, surcharge, maximum de clients ou redémarrage ; ou votre client a réutilisé une connexion périmée.

Correction : Alignez keepalive et timers d’inactivité, implémentez des retries avec jitter (prudemment), et vérifiez les métriques de saturation de l’upstream.

3) Symptom: Broken pipe spikes exactly during deploys

Cause racine : Pas d’arrêt gracieux, fenêtre de terminaison trop courte, readiness qui bascule trop tard, proxy qui envoie encore vers des instances en drainage.

Correction : Ajoutez une phase de drainage : retirez du routage, n’acceptez plus de nouveau travail, terminez le travail en cours, puis arrêtez. Ajustez les timeouts systemd.

4) Symptom: Broken pipe + 504 gateway timeouts

Cause racine : L’upstream est trop lent ou bloqué (CPU, IO, contention sur les verrous), timeout proxy trop court, ou mise en file.

Correction : Augmentez les timeouts seulement après avoir validé la marge de ressources. Préférez corriger la latence : réduire le travail, cacher, optimiser les requêtes, ajouter de la capacité.

5) Symptom: Broken pipe appears after enabling HTTP response streaming

Cause racine : Le streaming rend les abandons clients visibles ; les proxies peuvent aussi bufferiser de manière inattendue ou expirer les streams inactifs.

Correction : Configurez le streaming intentionnellement : désactivez le buffering quand nécessaire, envoyez des octets keepalive périodiques si approprié, ajustez les timeouts du proxy.

6) Symptom: Shell command prints “Broken pipe” in a pipeline

Cause racine : Le programme en aval sort tôt (p. ex. head), l’amont continue d’écrire et rencontre SIGPIPE/EPIPE.

Correction : Habituellement, ignorer. Si cela casse des scripts, redirigez stderr, ou utilisez des outils qui arrêtent proprement l’amont (ou gérez SIGPIPE).

7) Symptom: Many broken pipes after changing keepalive settings

Cause racine : Mismatch de timeout entre couches ; réutilisation de connexions mortes ; timeout LB plus court que proxy/app.

Correction : Documentez et alignez les timeouts d’inactivité bout-à-bout ; vérifiez par capture de paquets ; déployez progressivement les changements.

8) Symptom: Broken pipe plus OOM kills or restart storms

Cause racine : Le processus meurt en servant des requêtes ; les pairs continuent d’écrire sur des sockets morts.

Correction : Corrigez limites mémoire/fuites, réduisez la concurrence, mettez un backoff de redémarrage sensé, et implémentez des hooks d’arrêt gracieux.

Checklists / plan pas à pas

Checklist A: Decide if it’s noise or a fire

  1. Où est-ce loggé ? Logs proxy edge vs logs service interne vs scripts batch.
  2. Le taux change-t-il ? Un baseline stable est souvent du bruit ; une tendance à la hausse alerte.
  3. Y a-t-il des symptômes associés ? 5xx, 504, croissance de files, CPU steal, swap, latence IO, retransmits.
  4. Est-ce concentré ? Un endpoint, un upstream, un nœud, une AZ : investigatez cette tranche.
  5. S’aligne-t-il avec un deploy/restart ? Si oui, corrigez le drainage et le comportement d’arrêt d’abord.

Checklist B: Tighten timeouts and keepalive safely

  1. Inventoriez les timeouts à chaque saut : client, LB, reverse proxy, serveur d’app, clients de dépendances.
  2. Choisissez une cible : l’upstream doit expirer après le downstream, pas avant (généralement), et le LB ne devrait pas être le plus court sauf intention.
  3. Les idle keepalive doivent être cohérents ; sinon, attendez-vous à la réutilisation de connexions périmées.
  4. Déployez les changements sur un petit sous-ensemble ; surveillez resets/retransmits et le taux d’EPIPE.
  5. Documentez le « contrat » pour éviter qu’une prochaine optimisation ne casse tout de nouveau.

Checklist C: Make deploys stop causing broken pipes

  1. Assurez-vous que l’instance ne reçoit plus de trafic avant d’arrêter le processus.
  2. Implémentez une readiness qui bascule tôt (ne plus s’annoncer ready) et une liveness qui évite le flapping.
  3. Confirmez que la grâce systemd (ou orchestrateur) est suffisante pour les pires requêtes en vol.
  4. Logguez les phases d’arrêt : « draining started », « no longer accepting », « in-flight=… », « shutdown complete ».
  5. Testez en déployant sous charge et mesurez les erreurs visibles par le client, pas seulement les lignes de log.

Checklist D: Reduce broken-pipe noise without blinding yourself

  1. Taggez les logs avec du contexte (endpoint, upstream, octets envoyés, durée de requête).
  2. Échantillonnez les EPIPE répétitifs ; conservez la pleine fidélité pour les 5xx et timeouts.
  3. Gardez un compteur métrique pour EPIPE par composant ; alertez sur le taux de variation, pas sur l’existence.
  4. Conservez un petit échantillon brut de logs pour les enquêtes forensiques.

FAQ

1) Is “broken pipe” a Debian 13 bug?

Presque jamais. C’est une erreur UNIX/Linux standard remontée par les applications. Debian 13 peut changer des versions et des paramètres par défaut, ce qui modifie la visibilité, pas la physique.

2) Why do I see “BrokenPipeError: [Errno 32]” in Python?

Python lève BrokenPipeError quand un write rencontre EPIPE. Cela signifie généralement que le pair a fermé le socket (abandon client, timeout upstream, ou proxy qui coupe).

3) Why does it happen more with Nginx or reverse proxies?

Les proxies sont au milieu. Ils écrivent constamment vers clients et upstreams, et sont les premiers à remarquer quand un pair a disparu.
Ils loggent aussi ces conditions plus explicitement que beaucoup d’applis.

4) Should I disable SIGPIPE?

Beaucoup de serveurs réseau ignorent SIGPIPE pour qu’une déconnexion client ne tue pas le processus. C’est acceptable.
Ne le « désactivez » pas aveuglément dans des outils au hasard ; comprenez si vous souhaitez un crash (fail fast) ou un retour d’erreur (gérer proprement).

5) Does “broken pipe” mean data corruption?

Pas en soi. Cela signifie qu’un write n’a pas atteint un lecteur. Si vous streamez une réponse, le client a reçu une réponse partielle.
Pour des uploads ou RPC internes, vous avez besoin d’idempotence et de retries pour éviter des états partiels.

6) Why do I see it in shell pipelines when I use head?

Parce que head sort après avoir reçu suffisamment de lignes. Le processus amont continue d’écrire et rencontre un pipe fermé.
C’est normal ; redirigez stderr si ça vous dérange dans des scripts.

7) How do I tell whether the client or server closed first?

Utilisez une capture de paquets brève (direction FIN/RST) ou corrélez les logs : codes d’abandon client (comme 499) vs timeouts upstream (504) vs redémarrages de service.
En cas de doute, tcpdump pendant 60 secondes vaut mieux qu’un débat.

8) Should I just raise all timeouts to stop it?

Non. Augmenter les timeouts peut masquer la surcharge et accroître le blocage des ressources (plus de requêtes coincées, plus de mémoire, plus de sockets).
Corrigez la latence ou alignez les timeouts intentionnellement ; ne tournez pas simplement tous les réglages vers la droite.

9) Why does it spike during backups or large exports?

Les réponses longues amplifient l’impatience et la variabilité réseau. Une IO lente (disque, object store, base) augmente le temps de réponse, les clients expirent, puis les écritures rencontrent EPIPE.

10) Is it safe to filter “broken pipe” out of logs?

Parfois. Si vous avez démontré que c’est majoritairement des abandons clients et que vous disposez de métriques pour les variations de taux, l’échantillonnage ou le filtrage peut réduire le bruit.
Ne filtrez pas s’il est lié à des timeouts upstream, des appels internes ou des événements de déploiement.

Conclusion : prochaines étapes pour réduire le bruit et le risque

« Broken pipe » sur Debian 13 n’est ni une crise ni une raison de hausser les épaules. C’est un indice qu’un côté d’une conversation est parti plus tôt.
Votre travail est de décider si cette sortie anticipée était normale, un mauvais alignement de timeouts, ou un système sous pression.

Prochaines étapes pratiques qui rapportent :

  1. Classifier par source : proxy edge vs app vs dépendances internes.
  2. Corréler avec timeouts et saturation : si latence ou pression ressource augmentent, suivez cette piste d’abord.
  3. Aligner les timeouts bout-à-bout : client ↔ LB ↔ proxy ↔ app, et documenter le contrat.
  4. Corriger le drainage de déploiement : un arrêt gracieux coûte moins que l’analyse de logs.
  5. Dominer le bruit sans s’aveugler : échantillonnez les logs EPIPE répétitifs, mais alertez sur les changements de taux et sur les symptômes associés (504/5xx/redémarrages).

L’objectif n’est pas d’éliminer chaque ligne « broken pipe ». L’objectif est de faire en sorte que celles qui restent soient ennuyeuses, comprises, et ne soient pas le premier chapitre du cas n°76.

← Précédent
Proxmox LXC : réseau cassé — la checklist veth/tap qui identifie réellement la cause
Suivant →
Dovecot : maildir vs mdbox — choisissez un stockage qui ne vous hantera pas plus tard

Laisser un commentaire