Vos flux de tokens IA sont fragiles : guide du CTO pour des SSE reprenables, annulables et multi‑appareils

Par Diogo Hudson Dias
Engineer in a São Paulo office reviewing a dashboard of live AI token stream metrics showing reconnects and cancellations on dual monitors.

Votre interface LLM fait sans doute une superbe démo sur le Wi‑Fi du bureau. Puis arrive le monde réel : réseaux mobiles qui fluctuent, utilisateurs qui rafraîchissent en plein flux, allers‑retours de l’app entre premier et arrière‑plan, et personnes qui passent de l’ordinateur portable au téléphone. Soudain vous payez deux fois les tokens de sortie et les tickets support disent « ça mouline sans fin ».

Les Server‑Sent Events (SSE) restent la manière la plus pratique de livrer des flux de tokens depuis la plupart des fournisseurs. Ça s’entend bien avec les proxies, fonctionne sur HTTP/2 et ne requiert pas de duplex intégral comme WebSockets. Mais en sortie de boîte, SSE est fragile. La bonne nouvelle : on peut le renforcer. La récente vague de billets expliquant comment rendre les flux SSE reprenables, annulables et multi‑appareils a raison — et il est temps d’en faire un plan de mise en production que vous pouvez remettre à votre équipe.

Les modes de panne qui vous coûtent cher

  • Relances après abandon : Un utilisateur rafraîchit en pleine génération. Votre backend lance un nouvel appel au fournisseur. Vous venez de doubler la dépense pour la même requête.
  • Calcul fantôme après annulation : L’utilisateur appuie sur stop ; votre UI ferme la connexion TCP. Votre serveur ne propage pas l’annulation en amont, le fournisseur continue de générer — et de facturer.
  • Dérive multi‑appareils : Un utilisateur ouvre la même conversation sur web et mobile. Les deux déclenchent une génération. Vous payez deux fois, le contenu diverge, et réconcilier les deltas devient pénible.
  • Tamponnement des middleboxes : Les CDNs et proxies tamponnent votre flux « pour aider ». Le TTFT (time‑to‑first‑token) passe de 300–500 ms à plusieurs secondes. Les utilisateurs abandonnent.
  • Progrès perdus sur réseaux instables : 3–7 % des connexions SSE mobiles tomberont en 60 secondes. Sans reprise, vous relancez toute la génération.

Si votre produit émet 300 M de tokens de sortie/mois à 15 $ par million (typique pour une sortie de classe GPT‑4o), 8–12 % de duplication/gaspillage c’est 360–540 $/mois — faible sur le papier, mais ça corrèle avec le churn et la charge support. À l’échelle (milliards de tokens, ou modèles plus chers), l’addition grimpe vite. Plus important encore, des flux cassés détruisent la confiance.

Cadre de décision : quand SSE, quand WebSockets, quand WebRTC

  • SSE si vous n’avez besoin que du streaming serveur→client, de mécaniques de reconnexion simples et d’une large compatibilité proxy/CDN. La plupart des UIs LLM de type chat entrent dans ce cas.
  • WebSockets si vous avez besoin d’un vrai duplex pendant la génération (p. ex., avancement temps réel d’outils, interruptions utilisateur qui doivent passer instantanément à travers les NAT) et que vous contrôlez le chemin d’edge. Plus difficile à opérer à l’échelle d’Internet.
  • WebRTC si vous faites de l’audio/vidéo à faible latence avec franchissement de NAT. Utile pour les fonctionnalités de voix en streaming récemment lancées par de grands fournisseurs, mais excessif pour des tokens texte.

Ce billet part du principe que vous utilisez SSE. Si vous choisissez WebSockets ou WebRTC, la plupart des idées de plan de contrôle restent valables : IDs de flux, numérotation séquentielle, fan‑out et annulation.

L’architecture qui évite de payer deux fois

Ne laissez pas les clients se connecter directement à votre fournisseur de modèles. Insérez un Stream Broker léger dans votre pile :

  • Client ouvre un SSE vers votre Edge sur /v1/streams/:stream_id.
  • Edge/Ingress termine le TLS, désactive le buffering et achemine vers le Stream Broker (affinité par stream_id).
  • Stream Broker est un worker sans état avec un cache en mémoire de courte durée (ou Redis/NATS) indexé par stream_id. Il fait trois choses :
    • Multiplexage/fan‑out : Si une génération est déjà en cours pour ce stream_id, attachez le nouveau souscripteur ; ne démarrez pas un autre appel fournisseur.
    • Reprise : Tamponnez une fenêtre roulante de deltas récents avec un event.id strictement croissant. À la reconnexion avec Last-Event-ID, rejouez les parties manquantes.
    • Annulation : Sur stop utilisateur, annulez la requête fournisseur en amont via un client HTTP annulable. Si le dernier souscripteur se déconnecte, annulez automatiquement.
  • Model Worker détient les identifiants du fournisseur, demande des complétions en streaming et normalise les deltas spécifiques au fournisseur vers un format d’événements commun.

Conservez l’état du flux en mémoire 60–120 secondes après la fin/l’annulation pour absorber des changements rapides d’appareil ou des rafraîchissements d’onglet sans relancer la génération.

Identité de flux et séquencement

  • Dérivez stream_id de manière déterministe depuis conversation_id + turn_id + un epoch 32 bits. Si l’utilisateur édite le prompt ou l’état système, incrémentez l’epoch pour éviter les reprises obsolètes.
  • Émettez les champs SSE id comme des entiers strictement croissants par flux, en commençant à 1. Évitez les IDs de chunks du fournisseur ; renumérotez de votre côté.
  • Envoyez des événements avec des types : delta, tool, heartbeat, complete, error. Ne surchargez jamais data avec des signaux de contrôle.

Logique de reconnexion côté client (la seule que vous devriez avoir)

  1. En cas de coupure, reconnectez avec un backoff exponentiel plafonné à 2 secondes.
  2. Incluez Last-Event-ID avec le dernier id appliqué. Si le broker a encore le buffer, il rejoue ; sinon il renvoie un 410 Gone pour forcer une relance complète.
  3. Rendez l’affichage idempotent. Si vous voyez un id ancien, ignorez‑le ; si un id manque, demandez un replay ciblé via la query (?from_id=123) après reconnexion.

Une annulation qui arrête réellement la facturation

SSE est unidirectionnel. Fermer l’onglet du navigateur ne garantit pas que le fournisseur s’arrête. L’annulation doit circuler côté serveur :

  • Bouton Stop → POST /v1/streams/:stream_id/cancel avec un jeton à portée de flux et courte durée de vie (TTL 60 s). Ne vous fiez pas uniquement à la détection de fermeture de connexion.
  • Comportement d’annulation du Broker :
    • Marquez le flux comme cancelling ; notifiez tous les souscripteurs avec un event: error portant une raison typée (user_cancelled, superseded).
    • Abortez la requête HTTP du fournisseur (AbortController ou jeton d’annulation spécifique au client). Si le fournisseur ne supporte pas l’abort, repliez‑vous sur la fermeture du socket TCP.
    • Conservez les N derniers deltas en mémoire afin qu’une reprise rapide affiche le contenu partiel sans ré‑invoquer le modèle.
  • Single‑flight par défaut : Imposez une seule génération active par conversation. Un nouveau start annule automatiquement la précédente avec la raison superseded.

En pratique, une annulation robuste réduit de 15–35 % le nombre moyen de tokens de sortie consommés par tour de conversation, car les gens « rognent » souvent les réponses. C’est de l’argent bien réel sur des modèles premium et — surtout — une UX qui donne la sensation de contrôle.

Multi‑appareils sans double facturation

Un utilisateur qui passe de l’ordinateur portable au téléphone ne doit pas déclencher un nouvel appel modèle. Calculez une fois, diffusez à plusieurs :

  • Attachez plusieurs souscripteurs à un seul appel amont. Le broker considère chaque connexion client comme un souscripteur. Les nouveaux souscripteurs reçoivent un replay rapide des deltas tamponnés (selon Last-Event-ID) puis les tokens en direct.
  • Fan‑out au‑dessus d’un journal d’événements partagé. Ne persistez que les deltas structurés et les métadonnées (index de token, appels d’outils). Stockez le texte assemblé complet seulement à complete, ou réassemblez à la volée.
  • Faites respecter une politique de concurrence des appareils. Par exemple : multiples lecteurs autorisés, un seul initiateur. Les nouveaux initiateurs sur un autre appareil annulent automatiquement le flux en cours avec superseded.
  • Empêchez les doublons accidentels. Débouoncez les actions start pendant 300–500 ms par conversation pour éviter les doubles clics/départs en course.

Au fil d’audits menés pour des apps grand public aux États‑Unis, 5–12 % des appels LLM étaient dupliqués par des rafraîchissements d’onglet ou des bascules rapides d’appareil. Un fan‑out avec broker ramène cela à <2 %, et le p95 time‑to‑first‑token baisse typiquement de 30–50 % une fois le buffering corrigé.

Configuration de l’edge et des proxies (là où la plupart des équipes trébuchent)

Les middleboxes adorent tamponner. À vous de rendre le streaming non ambigu des en‑têtes HTTP jusqu’à l’origine. Configuration de base :

  • En‑têtes de réponse émis par le broker : Content-Type: text/event-stream, Cache-Control: no-cache, no-transform, Connection: keep-alive, X-Accel-Buffering: no (honoré par Nginx), et envoyez un commentaire de heartbeat : ping\n\n toutes les 10–15 secondes.
  • Nginx/Envoy : désactivez le proxy buffering pour text/event-stream, purgez de petits chunks, augmentez proxy_read_timeout pour couvrir les longues générations (p. ex., 120 s+). Assurez‑vous que HTTP/2 est activé côté client ; conservez les sémantiques de chunking.
  • CDN/Edge : beaucoup de CDNs tamponnent par défaut. Utilisez des options nommées « origin streaming », « no buffering » ou contournez le cache pour text/event-stream. Posez no-transform pour empêcher les modifications de contenu. Si votre fournisseur edge ne supporte pas le vrai streaming sur les offres gratuites, contournez‑le pour les chemins SSE.
  • Connexions au fournisseur : utilisez HTTP/2 quand c’est supporté ; réglez de petits buffers d’écriture pour purger les tokens rapidement. Certains fournisseurs batchent les tokens à moins que Accept: text/event-stream et stream=true ne soient posés précisément.

Particularités des fournisseurs à normaliser

  • Format des deltas de tokens : Les fournisseurs diffèrent entre trames partielles et texte complet. Normalisez en event: delta avec { id, text_delta, tool_calls_delta, usage? }. Assemblez le texte côté client.
  • Raisons d’arrêt : Exposez un ensemble cohérent : length, user_cancelled, content_filter, error. Faites correspondre les raisons spécifiques de chaque fournisseur aux vôtres.
  • Sémantique d’abort : Certaines APIs ne s’arrêtent que sur fermeture TCP ; d’autres acceptent une annulation à la portée de la requête. Standardisez via votre client HTTP et gardez un test d’intégration vérifiant un arrêt <1 seconde après cancel sur chaque fournisseur.
  • Comptage d’usage : Ne vous fiez pas uniquement au webhook d’usage asynchrone du fournisseur. Calculez vos propres comptes de tokens à partir des deltas pour la métrologie temps réel et la détection de fraude.

Sécurité : verrouillez les portées au flux, pas à l’utilisateur

  • Jetons à portée de flux : Émettez un JWT utilisable uniquement pour /v1/streams/:stream_id et /cancel, TTL ≤ 5 minutes. Encodez conversation_id, epoch, l’ID utilisateur et les actions permises.
  • Aucune PII dans les logs : Ne logguez jamais event.data. Logguez seulement id, event, tailles en octets et timings.
  • Clés fournisseur côté serveur uniquement. Les clients ne détiennent jamais les clés d’API des modèles. Le broker médie. Cela permet aussi l’annulation centrale et le fan‑out.
  • Limites de débit par conversation : Limitez à 1 la génération simultanée par défaut. Les politiques de burst se règlent au niveau organisation ou projet.

Résilience et modes dégradés

  • TTL de fenêtre de reprise : Conservez un ring buffer en mémoire de 60–120 secondes par flux (~64–256 KB suffisent). Si le buffer n’existe plus, renvoyez 410 Gone pour que le client sache relancer.
  • Commutateur de contournement de l’edge : Feature‑flaggez un chemin qui route le SSE directement vers l’origine si le CDN se met à tamponner pendant un incident.
  • Bascules de fournisseur : La bascule en milieu de flux est rarement propre. Préférez : détection rapide d’échec (1–2 s), annulez, puis redémarrez de façon transparente sur un fournisseur secondaire avec un indicateur UI clair « switched to backup model ».
  • Testez le chaos réseau : Injectez 3–5 % de perte de paquets, 250–600 ms de RTT variable et des déconnexions aléatoires de 1–3 s. Votre SSE doit rester lisible et reprenable.

Modèle de données : des flux adossés à des événements (event‑sourced) valent mieux que des chaînes ad hoc

Persistez chaque flux comme un journal append‑only d’événements structurés :

  • delta : { id, ts, text_delta, tool_delta?, usage_partial? }
  • heartbeat : { id, ts }
  • complete : { id, ts, stop_reason, usage_final }
  • error : { id, ts, code, retriable }

En lecture, vous pouvez reconstruire le texte final, auditer l’usage précisément et analyser les patterns d’interruption. Et si vous introduisez des A/B multi‑fournisseurs, vous voudrez cette traçabilité.

Observabilité : indicateurs (KPI) qui prouvent que ça marche

  • TTFT (time‑to‑first‑token) : ciblez <600 ms p50, <1,5 s p95 sur le haut débit ; mobile peut être plus élevé.
  • Taux de complétion des flux : pourcentage de flux se terminant en complete vs. error/cancel. Suivez par type d’appareil et de réseau.
  • Taux de reprise réussie : pourcentage de reconnexions qui rejouent les deltas manquants sans relance complète. Visez >90 % dans la fenêtre de reprise.
  • Suppression des doublons : part des tentatives de départs dupliqués qui ont été multiplexés dans un flux existant. Vous la voulez élevée.
  • Temps de propagation d’annulation : temps entre le clic utilisateur et l’accusé d’abort en amont. Visez <1 seconde.
  • Incidents de buffering à l’edge : repérez les pics de TTFT et les chutes soudaines de heartbeats ; alertez à la détection.

Un produit marketplace que nous avons accompagné a réduit de 9,2 % la consommation de tokens dupliqués et amélioré le p95 TTFT de 41 % après passage à un SSE brokerisé avec bonne configuration edge et reprise.

Plan de déploiement livrable ce trimestre

Semaine 0–2 : Validez le broker

  • Introduisez un Stream Broker minimal dans votre couche API. Normalisez les deltas du fournisseur, émettez du SSE avec id et ajoutez des heartbeats.
  • Désactivez le buffering de bout en bout sur une route canari. Mesurez TTFT et taux de chute.
  • Ajoutez /cancel et la propagation d’abort à une intégration fournisseur.

Semaine 3–5 : Rendez‑le durable

  • Ajoutez un ring buffer par flux (en mémoire d’abord ; Redis Streams si vous avez besoin de reprise inter‑instances).
  • Implémentez la reprise Last-Event-ID et le replay rapide. Débouoncez les départs dupliqués.
  • Faites du fan‑out vers plusieurs souscripteurs. Vérifiez la bascule multi‑appareils sans double facturation.

Semaine 6–8 : Mettez en place garde‑fous et métriques

  • JWT à portée de flux, limites de débit et single‑flight par conversation.
  • Tableaux de bord pour TTFT, succès de reprise, latence d’annulation, suppression des doublons.
  • Tests de chaos avec perte de paquets artificielle et déconnexions intermittentes. Corrigez jusqu’à stabilisation des métriques.

Arbitrages et quand ne pas le faire

  • Complexité : Un broker ajoute des pièces mobiles. Si votre app est interne, sur réseaux stables et avec de faibles volumes, vous pouvez vivre avec un SSE naïf.
  • Latence vs. cohérence : Les fenêtres de replay et la normalisation ajoutent quelques millisecondes. Ça vaut la durabilité ; mesurez pour en être sûr.
  • Gestion d’état : Persister des deltas ajoute du stockage et du risque PII. Gardez des TTL courts, chiffrez au repos et évitez de logger les payloads.
  • Contraintes fournisseur : Certains vendors ont des sémantiques de streaming incohérentes. Normalisez agressivement et bâtissez des tests de contrat par API de modèle.

Pourquoi maintenant

Deux choses ont changé en 2026. D’abord, les fonctionnalités LLM arrivent sur mobile et en faible bande passante, pas seulement sur le web desktop. Ensuite, la « voix intelligente » des modèles et des événements d’outils plus riches augmentent la surface de livraison partielle et d’annulation. Vous n’avez pas besoin de plus d’agents ; vous avez besoin d’un contrôle de flux à la couche stream. Faites cela correctement et vous réduisez le gaspillage, gagnez en vitesse et — surtout — vous rendez vos fonctionnalités IA fiables.

Points clés

  • Mettez un Stream Broker entre clients et fournisseurs pour multiplexer, reprendre et annuler de manière centrale.
  • Utilisez un stream_id déterministe + un event.id strictement croissant ; reprenez avec Last-Event-ID.
  • Implémentez un /cancel explicite et propagez l’abort côté serveur ; ne comptez pas sur la fermeture des sockets.
  • Désactivez le buffering du CDN à l’origine ; envoyez des heartbeats et les bons en‑têtes SSE.
  • Faites du fan‑out d’une génération amont vers plusieurs appareils ; débouoncez les départs dupliqués.
  • Suivez TTFT, succès de reprise, latence d’annulation et suppression des doublons pour prouver l’impact.
  • Attendez‑vous à 15–35 % de tokens de sortie en moins par tour et 30–50 % de latence perçue en moins quand c’est bien fait.

Author: Diogo Hudson Dias

Ready to scale your engineering team?

Tell us about your project and we'll get back to you within 24 hours.

Start a conversation