Communication Temps Réel

Socket.IO

Socket.IO est un framework d'événements en temps réel basé sur WebSockets. Il ajoute la gestion des salles (rooms), les accusés de réception, la reconnexion automatique et un repli vers le polling, vous permettant de vous concentrer sur vos fonctionnalités plutôt que sur la plomberie des connexions.

intermediate14 min readUpdated 16 sept. 2026
server.ts
ts
// server.ts
import { Server } from "socket.io";

const io = new Server(httpServer, {
  cors: { origin: "https://app.example.com" },
});

io.use((socket, next) => {
  const user = verifyToken(socket.handshake.auth.token);
  if (!user) return next(new Error("unauthorized"));
  socket.data.user = user;
  next();
});

io.on("connection", (socket) => {
  socket.join(`user:${socket.data.user.id}`);

  socket.on("message:send", (payload, ack) => {
    io.to(payload.room).emit("message:new", payload);
    ack({ ok: true });
  });
});
Sortie
2010
S'exécute sur
Node.js
Transport
WebSocket avec repli polling
Protocole
Engine.IO
Mise à l'échelle via
Redis adapter
Client
Navigateur et Node.js

Pourquoi c'est important

Ce que Socket.IO vous apporte dès le premier jour

Salles et espaces de noms (namespaces)

Groupez les sockets côté serveur et émettez vers une salle, un espace de noms ou tout le monde sauf l'expéditeur. Plus besoin de gérer manuellement le suivi des connexions, et cela s'adapte à plusieurs instances.

Reconnexion intégrée

Le client se reconnecte avec un délai exponentiel (backoff) après une perte de connexion et peut récupérer les paquets manqués, évitant ainsi que les réseaux mobiles ou la mise en veille d'un ordinateur ne cassent l'application.

Événements avec accusés de réception

Chaque émission peut transporter un callback, transformant le cycle requête/réponse sur un socket en un appel de fonction plutôt qu'en un jeu de devinettes.

Le tableau complet

Les trois couches de Socket.IO

Une couche d'événements pour votre application, une couche de transport qui débute en HTTP polling, et une couche d'adaptateur pour passer à l'échelle sur plusieurs instances.

Événements

Émettre

Tout est un événement nommé. Le serveur et le client émettent et écoutent sur les mêmes noms de canaux, et les données sont du JSON pur.

Transport

Mise à niveau

La connexion commence en HTTP long polling et passe en WebSocket, ce qui lui permet de franchir les proxys, les anciens navigateurs et les réseaux restrictifs.

Adaptateur

Échelle

Un adaptateur diffuse les événements entre les instances du serveur, permettant ainsi à plusieurs pods de servir une seule application temps réel logique.

HTML5 en un coup d'oeil

Les composants que vous utiliserez

Événements

socket.emit et socket.on déplacent des données nommées dans les deux sens.

Salles

socket.join(room) et io.to(room).emit(...) ciblent un sous-ensemble d'utilisateurs.

Espaces de noms

Divisez une seule connexion en canaux isolés comme /chat et /admin.

Reconnexion

Reconnexion automatique avec backoff et récupération d'état optionnelle.

Accusés de réception

Un callback confirme que le serveur a reçu et traité un événement.

Redis adapter

Partagez les diffusions entre instances et maintenez des sessions collantes (sticky sessions).

Flux

Le flux d'un message en temps réel

Du handshake à la déconnexion, chaque interaction Socket.IO suit le même chemin court.

  1. 1

    Connexion

    Le client ouvre une connexion et présente ses identifiants dans le payload d'authentification du handshake.

  2. 2

    Rejoindre des salles

    Le serveur assigne le socket à des salles en fonction de l'utilisateur authentifié, du tenant et des abonnements.

  3. 3

    Émission

    Un client émet un événement nommé avec un payload JSON et, optionnellement, un callback pour la réponse.

  4. 4

    Diffusion

    Le serveur traite l'événement et émet vers la salle concernée, afin que chaque socket intéressé le reçoive.

  5. 5

    Déconnexion

    Lorsque la connexion se ferme, le socket quitte automatiquement toutes ses salles et le serveur effectue le nettoyage.

Le guide complet

Socket.IO: Tout ce que vous devez savoir

Ce que Socket.IO apporte aux WebSockets

Socket.IO est une bibliothèque dédiée à la communication en temps réel basée sur des événements entre un serveur et ses clients. En arrière-plan, elle utilise le protocole WebSocket lorsque c’est possible et bascule sur le HTTP long polling dans le cas contraire. Par-dessus ce transport, elle vous offre un petit protocole d’application : des événements nommés, des rooms, des accusés de réception (acknowledgements), la reconnexion automatique et des heartbeats.

C’est précisément tout l’intérêt de cette couche supplémentaire. Une connexion WebSocket brute vous fournit un canal de communication, et rien d’autre. Chaque application réelle doit alors recréer les mêmes fonctionnalités : comment grouper les connexions par salon de discussion ou par tenant, comment savoir si un message a été reçu, comment se reconnecter proprement, comment diffuser un message sur plusieurs serveurs ? Socket.IO répond à ces questions une fois pour toutes, via une bibliothèque éprouvée, afin que vous puissiez vous concentrer sur votre produit.

Le compromis est que Socket.IO n’est pas un WebSocket standard. Il définit son propre handshake et son propre format de paquet au-dessus d’Engine.IO ; par conséquent, un client WebSocket natif ne peut pas se connecter à un serveur Socket.IO. Les deux parties doivent utiliser le client Socket.IO. Si vous avez besoin d’une interopérabilité avec des clients WebSocket arbitraires, utilisez plutôt le guide WebSockets et la bibliothèque ws.

La suite de ce guide part du principe que vous connaissez déjà le protocole brut et que vous souhaitez désormais utiliser la version “batteries-included”.

Configuration du serveur et du client

Le serveur s’attache à un serveur HTTP existant, qui est généralement le même que celui servant votre API. Cela signifie un seul port, un seul certificat TLS et aucune infrastructure supplémentaire.

import { Server } from "socket.io";
import { createServer } from "node:http";

const httpServer = createServer(app);

const io = new Server(httpServer, {
  cors: { origin: process.env.APP_ORIGIN, credentials: true },
});

io.on("connection", (socket) => {
  console.log("connected", socket.id);
});

httpServer.listen(3000);

Le client se connecte avec la même origine et s’authentifie via le handshake.

import { io } from "socket.io-client";

const socket = io("https://api.example.com", {
  auth: { token: getAccessToken() },
  withCredentials: true,
});

socket.on("connect", () => console.log("connected", socket.id));
socket.on("disconnect", (reason) => console.log("closed", reason));

Le socket.id est un identifiant par connexion. Il est utile pour les logs et pour cibler une connexion spécifique, mais il change lors d’une reconnexion ; ne l’utilisez donc jamais comme identifiant utilisateur et ne le stockez pas comme un état durable.

Événements, accusés de réception et callbacks

Dans Socket.IO, tout est un événement nommé transportant une charge utile JSON. Le serveur et le client utilisent tous deux emit pour envoyer et on pour écouter. Les noms sont de simples chaînes de caractères, choisissez donc une convention et tenez-vous-y — noun:verb comme message:send, message:new et presence:joined est très lisible des deux côtés.

La fonctionnalité qui distingue Socket.IO d’un socket brut est l’accusé de réception (acknowledgement). Si l’émetteur passe un callback en dernier argument, le destinataire peut l’appeler pour répondre, transformant ainsi l’émission en une requête/réponse sur la même connexion.

// client
socket.emit("message:send", { room: "general", body: "hello" }, (ack) => {
  if (!ack.ok) showError(ack.error);
});

// server
socket.on("message:send", (payload, ack) => {
  if (!payload.body) return ack({ ok: false, error: "empty" });
  io.to(payload.room).emit("message:new", payload);
  ack({ ok: true, at: Date.now() });
});

Utilisez les accusés de réception pour tout événement dont le client doit connaître l’issue : création d’un enregistrement, jointure à une room, soumission d’un formulaire. Pour les diffusions pures (broadcasts) où personne n’attend de réponse, un simple emit suffit. Un compromis est socket.timeout(5000).emit(...), qui déclenche une erreur dans le callback si aucun accusé de réception n’arrive à temps.

Middleware et le pipeline d’événements

Socket.IO dispose de deux couches de middleware, et placer votre logique dans la bonne couche permet de garder vos handlers propres.

Le middleware de connexion, enregistré avec io.use ou namespace.use, s’exécute une seule fois par connexion avant l’événement connection. C’est ici que doivent se trouver l’authentification, la résolution du tenant et la configuration par connexion. Les middlewares s’exécutent dans l’ordre d’enregistrement, et chacun appelle next() pour continuer ou next(new Error(...)) pour rejeter la connexion.

io.use((socket, next) => {
  const startedAt = Date.now();

  socket.on("disconnect", () => {
    metrics.observe("socket.duration", Date.now() - startedAt);
  });

  next();
});

Le middleware d’événement, enregistré avec socket.use, s’exécute pour chaque événement entrant sur ce socket. C’est l’endroit idéal pour la validation, le rate limiting et la journalisation structurée, car il a accès au nom de l’événement et à sa charge utile (payload) avant tout handler.

const chat = io.of("/chat");

chat.use((socket, next) => {
  if (!socket.data.user) return next(new Error("unauthorized"));
  next();
});

chat.use((socket, next) => {
  socket.onAny((event, ...args) => {
    logger.info({ event, userId: socket.data.user.id, args });
  });
  next();
});

socket.onAny observe chaque événement entrant, et socket.onAnyOutgoing observe tout ce que vous envoyez, ce qui vous permet d’obtenir une trace complète d’une connexion sans modifier un seul handler. Le middleware est également conscient des namespaces : le namespace /admin peut exiger un rôle différent sans affecter /chat.

Rooms et namespaces

Deux mécanismes de regroupement permettent de cibler les messages plutôt que de les diffuser à tout le monde.

Une room est une étiquette côté serveur appliquée à un ensemble de sockets. N’importe quelle socket peut rejoindre ou quitter n’importe quelle room à tout moment, et une socket peut appartenir à plusieurs rooms simultanément. Lorsque vous émettez un événement vers une room, seuls ses membres le reçoivent.

io.on("connection", (socket) => {
  socket.join(`user:${socket.data.user.id}`);

  socket.on("room:join", (room, ack) => {
    socket.join(room);
    ack({ ok: true });
  });
});

Un namespace est un canal de communication distinct accessible via un chemin, tel que /chat ou /admin. Les namespaces possèdent leur propre middleware, leurs propres gestionnaires de connexion et leurs propres rooms. Utilisez les namespaces pour séparer des domaines qui ne doivent absolument pas partager d’événements — par exemple, un namespace pour un chat public et un autre pour l’administration interne — plutôt que pour modéliser des données au sein d’une même fonctionnalité.

Les rooms sont l’outil principal. Modélisez-les en fonction des éléments qui importent pour vos utilisateurs : une conversation, un document, un tenant, un tableau de bord. Ainsi, la diffusion devient une simple ligne de code au lieu d’une boucle sur toutes les connexions.

Diffusion et ciblage

Socket.IO utilise un vocabulaire précis pour définir qui reçoit un événement ; bien le maîtriser permet d’éviter à la fois les fuites de données et le gaspillage de ressources lors de la diffusion.

  • io.emit(...) — tous les sockets connectés. C’est rarement ce que l’on souhaite.
  • socket.emit(...) — uniquement le socket qui gère l’événement actuel.
  • socket.broadcast.emit(...) — tout le monde, sauf l’expéditeur.
  • socket.to(room).emit(...) — tout le monde dans la room, sauf l’expéditeur.
  • io.to(room).emit(...) — tout le monde dans la room, y compris l’expéditeur.
  • io.to(roomA).to(roomB).emit(...) — l’union des deux rooms.
  • socket.to(socketId).emit(...) — un socket spécifique via son id.
socket.on("typing", ({ room }) => {
  // Tell the room, but not the person typing.
  socket.to(room).emit("typing", { user: socket.data.user.id });
});

Le chaînage de to permet d’unir les destinataires ; il n’existe pas d’opérateur d’intersection dans l’API native. Si vous avez besoin de cibler les « membres de la room A qui sont aussi administrateurs », modélisez cela comme une room distincte — room:${id}:admins — plutôt que d’essayer de le calculer au moment de l’émission.

Authentifier le handshake

L’authentification doit avoir lieu lors du handshake de connexion, et non dans un premier message. Le middleware Socket.IO enregistré avec io.use s’exécute avant l’événement connection, ce qui vous permet de rejeter un socket non authentifié avant qu’il ne puisse émettre quoi que ce soit ou rejoindre un salon.

io.use((socket, next) => {
  const token = socket.handshake.auth.token;

  try {
    const payload = verifyToken(token);
    socket.data.user = { id: payload.sub, roles: payload.roles };
    next();
  } catch {
    next(new Error("unauthorized"));
  }
});

Deux détails sont importants. Premièrement, socket.data est l’endroit idéal pour attacher l’état propre à chaque connexion ; cet état persiste pendant toute la durée de la connexion et est disponible dans chaque handler. Deuxièmement, une connexion rejetée déclenche un événement connect_error côté client, permettant ainsi à l’UI de demander un nouveau jeton au lieu de tenter une reconnexion indéfiniment.

Pour les clients navigateurs, un cookie envoyé lors du handshake est une alternative à un jeton explicite, et vous permet de réutiliser votre infrastructure de session existante. Dans tous les cas, configurez toujours l’origine cors sur votre propre application, et autorisez chaque événement en fonction de l’utilisateur dans socket.data — l’authentification prouve qui s’est connecté, pas ce qu’il est autorisé à faire.

Reconnexion et récupération de l’état de connexion

Les connexions tombent. Les ordinateurs portables se mettent en veille, les téléphones changent de réseau, les load balancers redémarrent. Socket.IO gère la reconnexion automatiquement avec un backoff exponentiel et du jitter, et il émet les événements reconnect_attempt et reconnect pour vous permettre d’afficher l’interface utilisateur appropriée.

Par défaut, cependant, les événements envoyés pendant l’absence du client sont perdus. Le client se reconnecte comme un nouveau socket et reprend à partir de maintenant. C’est acceptable pour un flux en direct, mais incorrect pour un chat ou un document collaboratif.

La récupération de l’état de connexion (connection state recovery) résout le cas des interruptions courtes. Activez-la sur le serveur, et un client qui se reconnecte en présentant son session id recevra les paquets qu’il a manqués, à condition que la déconnexion ait été brève.

const io = new Server(httpServer, {
  connectionStateRecovery: {
    maxDisconnectionDuration: 2 * 60 * 1000,
    skipMiddlewares: true,
  },
});

La récupération est délibérément limitée : elle conserve les paquets récents en mémoire pendant une courte fenêtre et uniquement sur l’instance qui a géré la connexion originale ; ce n’est donc pas un substitut à un stockage durable. Tout ce qui doit survivre à une panne plus longue — messages, commandes, modifications de documents — doit être stocké dans une base de données, le socket ne servant qu’à notifier les clients qu’un changement a eu lieu.

Passer à l’échelle avec l’adaptateur Redis

Un serveur Socket.IO conserve ses sockets et ses rooms en mémoire. Si vous lancez deux instances derrière un load balancer, elles se comportent comme deux applications temps réel distinctes : un message émis sur l’instance A n’atteindra jamais les clients de l’instance B.

L’adaptateur Redis règle ce problème. Chaque instance publie ses broadcasts sur un canal Redis pub/sub et s’abonne à ce même canal ; ainsi, un emit sur une instance est délivré aux sockets correspondantes partout.

import { createAdapter } from "@socket.io/redis-adapter";
import { createClient } from "redis";

const pubClient = createClient({ url: process.env.REDIS_URL });
const subClient = pubClient.duplicate();

await Promise.all([pubClient.connect(), subClient.connect()]);

const io = new Server(httpServer, {
  adapter: createAdapter(pubClient, subClient),
});

Deux notes opérationnelles. Premièrement, les sticky sessions restent nécessaires pendant la phase de polling HTTP, car le handshake Engine.IO s’étend sur plusieurs requêtes qui doivent impérativement atteindre la même instance. Configurez le load balancer pour une affinité basée sur les cookies, ou forcez l’utilisation exclusive du transport WebSocket. Deuxièmement, l’adaptateur utilise Redis pub/sub, ce qui est rapide mais non durable : un message publié pendant qu’une instance redémarre sera perdu. Redis fait également l’objet de son propre guide Redis.

Pour les déploiements très volumineux, il existe un adaptateur sharded qui répartit les canaux sur un cluster Redis, ainsi que des adaptateurs pour d’autres brokers, mais commencez par l’adaptateur standard.

Émettre depuis l’extérieur d’un socket

Les événements en temps réel proviennent rarement de l’intérieur d’un gestionnaire de connexion. Une requête HTTP crée un commentaire, un worker termine un rapport, un webhook confirme un paiement — tous ces événements doivent notifier les clients connectés.

Gardez une référence vers io et émettez vers une room depuis n’importe où :

app.post("/comments", async (req, res) => {
  const comment = await db.comment.create({ data: req.body });

  io.to(`post:${comment.postId}`).emit("comment:new", comment);

  res.status(201).json(comment);
});

Les rooms sont l’outil idéal ici car elles sont partagées entre les instances lorsque l’adapter est configuré. Pour atteindre un utilisateur spécifique, émettez vers une room propre à l’utilisateur — user:${id} — que le socket a rejointe lors de la connexion, plutôt que de suivre les socket ids. Cela continue de fonctionner même si l’utilisateur a deux onglets ouverts, se reconnecte ou atterrit sur une instance différente.

Si vous avez réellement besoin d’un socket id, io.in(room).fetchSockets() renvoie les sockets actifs dans une room, ce qui est utile pour le comptage de présence et les envois ciblés.

Présence, indicateurs de saisie et fonctionnalités collaboratives

L’utilisation de rooms combinée à un store partagé couvre la majorité des fonctionnalités collaboratives. La gestion de la présence — savoir qui est en ligne — est le cas le plus courant, et une version naïve cesse de fonctionner dès que vous lancez plus d’une instance.

Le pattern consiste à maintenir la présence faisant foi dans Redis, et non dans une map JavaScript, tout en effectuant un nettoyage lors de la déconnexion :

io.on("connection", async (socket) => {
  const { id } = socket.data.user;
  await redis.sadd("online", id);

  socket.on("disconnect", async () => {
    const sockets = await io.in(`user:${id}`).fetchSockets();
    if (sockets.length === 0) await redis.srem("online", id);
  });
});

La vérification fetchSockets est cruciale : un utilisateur ayant deux onglets ouverts ne doit pas apparaître hors ligne lorsqu’il ferme l’un d’entre eux. Les indicateurs de saisie, les positions du curseur et les flags “quelqu’un est en train de modifier” suivent la même logique : un état éphémère diffusé à une room, avec une expiration courte pour éviter qu’un client planté ne laisse un indicateur obsolète indéfiniment.

Pour l’édition collaborative réelle avec résolution de conflits, tournez-vous vers une bibliothèque CRDT telle que Yjs et envoyez ses mises à jour via le socket, plutôt que d’inventer votre propre algorithme de fusion.

Validation, limitation du débit et sécurité

Un socket est un canal d’entrée non fiable, exactement comme une requête HTTP. Considérez chaque payload d’événement comme hostile jusqu’à ce qu’il soit validé.

  • Validez les structures. Analysez les payloads avec une bibliothèque de schéma comme Zod avant de les manipuler, et rejetez-les avec un accusé de réception au lieu de lever une exception.
  • Limitez le débit (Rate limiting). Un socket peut émettre des milliers d’événements par seconde. Utilisez un système de “token bucket” par socket et déconnectez ou bridez les utilisateurs abusifs.
  • Plafonnez la taille des payloads. maxHttpBufferSize définit la quantité de données qu’un seul message peut transporter.
  • Autorisez chaque événement. Vérifiez à nouveau que l’utilisateur dans socket.data est autorisé à agir sur la room ou la ressource, et ne vous contentez pas de vérifier qu’il est connecté.
  • Validez l’origine. Configurez cors.origin et ne le laissez pas ouvert en production.
  • Gérez les timeouts. Les heartbeats et les timeouts d’inactivité empêchent les fuites de connexions abandonnées.
io.on("connection", (socket) => {
  socket.use(([event, payload], next) => {
    const parsed = MessageSchema.safeParse(payload);
    if (!parsed.success) return next(new Error("invalid_payload"));
    if (!takeToken(socket.id)) return next(new Error("rate_limited"));
    next();
  });
});

Le middleware par socket enregistré avec socket.use est l’endroit idéal pour centraliser ces vérifications, afin que les handlers individuels restent concentrés sur la logique métier.

Débogage et observabilité

Le premier outil est intégré. Le fait de configurer DEBUG=socket.io:* (ou engine*) affiche le handshake, les montées en version du transport (transport upgrades) et le flux de paquets, ce qui est généralement suffisant pour diagnostiquer un client qui ne parvient pas à se connecter.

Pour la production, suivez les signaux qui prédisent réellement les incidents :

  • Sockets connectées — une chute soudaine indique un déploiement, un crash ou une partition réseau.
  • Événements par seconde, par nom — un pic sur un événement spécifique est souvent le signe d’une boucle infinie côté client.
  • Raisons de déconnexionping timeout et transport close signalent des problèmes de réseau ou de proxy, tandis que io server disconnect signifie que votre code a fermé la socket.
  • Santé de l’adapter — si le pub/sub Redis est lent ou déconnecté, les diffusions entre instances s’arrêtent sans qu’aucune socket ne semble échouer.
io.on("connection", (socket) => {
  socket.on("disconnect", (reason) => {
    metrics.increment("socket.disconnect", { reason });
  });
});

Le package @socket.io/admin-ui ajoute un tableau de bord basé sur ces mêmes données ; il est pertinent de l’exécuter en staging pour visualiser les rooms et les sockets en temps réel. Comme pour tout système temps réel, les pannes les plus déroutantes sont partielles : une instance fonctionne correctement alors qu’une autre ne fonctionne pas, et seule une vue par instance permet de le révéler.

Tester un serveur en temps réel

Le code temps réel est testable. Démarrez le serveur sur un port éphémère, connectez quelques instances socket.io-client et vérifiez les événements qu’elles reçoivent. La discipline essentielle consiste à attendre les événements plutôt qu’à utiliser des pauses (sleep), afin que les tests soient rapides et déterministes.

import { io as Client } from "socket.io-client";

test("broadcasts a message to the room", async () => {
  const a = Client(url, { auth: { token } });
  const b = Client(url, { auth: { token } });

  await Promise.all([once(a, "connect"), once(b, "connect")]);

  a.emit("room:join", "r1");
  b.emit("room:join", "r1");

  const received = once(b, "message:new");
  a.emit("message:send", { room: "r1", body: "hi" });

  const [message] = await received;
  expect(message.body).toBe("hi");

  a.close();
  b.close();
});

Testez également les scénarios d’échec, car c’est là que se cachent les bugs du temps réel : un client non authentifié devrait recevoir connect_error, une charge utile (payload) incorrecte devrait être rejetée par un accusé de réception, et un socket qui se déconnecte devrait quitter ses salons. Utilisez un salon ou un espace de noms (namespace) unique par test pour que les tests parallèles ne voient pas les événements des autres, et fermez toujours les clients pour que le processus de test puisse s’arrêter.

Déployer un serveur Socket.IO

Le déploiement de Socket.IO est un déploiement HTTP avec deux exigences supplémentaires : des connexions persistantes et un état partagé. La plupart des imprévus proviennent de l’oubli de l’un de ces points.

  • Un seul port. Attachez Socket.IO au même serveur HTTP que votre API et gérez la terminaison TLS au niveau du proxy. Il n’y a pas de port séparé à exposer.
  • Support du proxy pour les upgrades. Nginx et la plupart des load balancers nécessitent une configuration explicite pour transmettre les headers Upgrade et Connection ; sans cela, la connexion reste silencieusement en mode polling.
  • Sessions collantes (Sticky sessions). L’affinité basée sur les cookies permet de maintenir le handshake de polling sur une seule instance. Si vous forcez le transport WebSocket-only, l’affinité est moins critique, mais le handshake doit tout de même s’achever quelque part.
  • Adaptateur Redis. Configurez-le avant même que la deuxième instance n’existe, et non après que les utilisateurs aient signalé des messages manquants.
  • Arrêt progressif (Graceful shutdown). Sur SIGTERM, arrêtez d’accepter de nouvelles connexions et fermez le serveur pour permettre aux événements en cours de se terminer.
process.on("SIGTERM", async () => {
  io.close(); // disconnects clients and stops the server
  await pubClient.quit();
  await subClient.quit();
  httpServer.close();
});
upstream io_nodes {
  ip_hash;
  server 10.0.0.1:3000;
  server 10.0.0.2:3000;
}

Dimensionnez chaque instance en fonction des connexions qu’elle supporte, et pas seulement selon le débit de requêtes. Chaque socket consomme de la mémoire et un descripteur de fichier, et une instance unique avec des dizaines de milliers de connexions échouera d’une manière qu’un service basé sur des requêtes ne connaîtrait jamais. Passez à l’échelle horizontalement dès que possible, surveillez le nombre de connexions et accordez aux déploiements une période de grâce suffisamment longue pour que les clients puissent se reconnecter à une instance saine.

Quand les WebSockets bruts sont le meilleur choix

Socket.IO n’est pas toujours la solution idéale. Privilégiez le protocole brut lorsque :

  • L’interopérabilité est primordiale. Les clients WebSocket natifs, les autres langages et les outils de protocole stricts ne peuvent pas gérer le handshake personnalisé de Socket.IO.
  • Vous avez besoin du client le plus léger possible. Socket.IO nécessite l’installation d’un bundle client, alors que les WebSockets bruts sont intégrés nativement au navigateur.
  • Votre infrastructure est nativement compatible WebSocket. Certaines passerelles, brokers et runtimes edge terminent les connexions WebSocket, mais ne supportent pas le fallback de polling de Socket.IO.
  • Vous contrôlez les deux extrémités et ne voulez aucune abstraction. Si la gestion des rooms et la reconnexion sont triviales pour votre cas d’usage, ws est plus simple à appréhender.

À l’inverse, choisissez Socket.IO si vous avez besoin de rooms, d’accusés de réception (acknowledgements), de reconnexion automatique et de diffusion multi-instances sans avoir à les développer vous-même. Pour la plupart des équipes produit, cette liste représente l’intégralité des fonctionnalités de leur couche temps réel, et c’est précisément pour cela que cette bibliothèque existe.

Bonnes pratiques

  • Authentifiez-vous dans io.use et stockez l’utilisateur dans socket.data, et non dans une closure.
  • Modélisez vos rooms selon des objets de domaine réels — conversations, documents, tenants.
  • Utilisez des acquittements (acknowledgements) pour les événements dont le client a besoin du résultat.
  • Privilégiez socket.to(room) lorsque l’expéditeur ne doit pas recevoir son propre événement.
  • Activez la récupération de l’état de connexion, mais conservez l’état durable dans une base de données.
  • Ajoutez l’adaptateur Redis avant d’ajouter une seconde instance, et configurez les sessions collantes (sticky sessions).
  • Émettez depuis les handlers HTTP et les workers via des rooms, et non via des socket ids stockés.
  • Validez, autorisez et limitez le débit (rate limit) de chaque événement.
  • Nettoyez la présence dans disconnect et utilisez fetchSockets pour gérer les onglets multiples.
  • Définissez explicitement maxHttpBufferSize, les origines CORS et les timeouts.

Erreurs courantes

  • Appeler io.emit alors que seul un room devrait recevoir l’événement.
  • Supposer que Socket.IO et WebSockets sont interchangeables, et échouer lors de la connexion d’un client natif.
  • Faire confiance à socket.handshake.auth sans vérifier le token.
  • Stocker la présence dans un Map local et la perdre derrière un load balancer.
  • Oublier les sticky sessions et casser le handshake du polling.
  • S’attendre à ce que la reconnexion rejoue les événements sans récupération de l’état de connexion.
  • Utiliser socket.id comme identifiant utilisateur, puis subir un crash lors de la reconnexion.
  • Effectuer des tâches lourdes ou bloquantes à l’intérieur d’un gestionnaire d’événements, ce qui ralentit tous les sockets de l’instance.
  • Laisser la validation du payload et le rate limiting au frontend.
  • Traiter le socket comme un stockage durable pour des données qui ne doivent pas être perdues.

Pour aller plus loin

Socket.IO est une couche de haut niveau basée sur le protocole abordé dans le guide WebSockets, qui explique les frames, les heartbeats et le handshake d’upgrade que vous abstrayez désormais. Le guide Redis approfondit le pub/sub et l’état partagé derrière l’adapter, tandis que Node.js Basics explique l’event loop sur laquelle s’exécute chaque handler. Si votre serveur Socket.IO est rattaché à une API HTTP, le guide Express couvre le routing et le middleware avec lesquels il partage le même processus.

En pratique

Serveur, client, salles et mise à l'échelle

Les quatre fichiers derrière la plupart des fonctionnalités temps réel.

server.ts
import { Server } from "socket.io";
import { createServer } from "node:http";

const httpServer = createServer(app);

const io = new Server(httpServer, {
  cors: {
    origin: process.env.APP_ORIGIN,
    credentials: true,
  },
});

io.use((socket, next) => {
  const token = socket.handshake.auth.token;
  try {
    socket.data.user = verifyToken(token);
    next();
  } catch {
    next(new Error("unauthorized"));
  }
});

io.on("connection", (socket) => {
  socket.join(`user:${socket.data.user.id}`);
  socket.emit("ready", { id: socket.id });
});

httpServer.listen(3000);

Salles Socket.IO vs canaux WebSocket bruts

Socket.IO suit l'appartenance aux salles pour vous. Avec le protocole brut, vous devez maintenir une map de connexions et distribuer les messages manuellement, ce qui est source d'erreurs subtiles.

Socket.IO
socket.join(`org:${orgId}`);
io.to(`org:${orgId}`).emit("update", payload);
WebSocket brut
// You own membership, fan-out and cleanup.
const rooms = new Map<string, Set<WebSocket>>();
for (const ws of rooms.get(`org:${orgId}`) ?? []) {
  if (ws.readyState === ws.OPEN) ws.send(payload);
}

Accusé de réception vs émission sans retour

Un callback transforme une émission en requête/réponse et fait remonter les erreurs. L'émission sans retour convient pour les diffusions pures, mais pas pour des actions pouvant être rejetées.

Avec ack
socket.emit("order:create", order, (result) => {
  if (!result.ok) showError(result.error);
  else markCreated(result.id);
});
Sans retour
socket.emit("order:create", order);
// No idea whether the server accepted,
// rejected or crashed.

Compromis

Socket.IO est-il l'abstraction appropriée ?

Socket.IO échange une légère couche de protocole et l'obligation d'utiliser son client contre une quantité massive de plomberie temps réel que vous devriez sinon écrire vous-même.

Strengths

  • Fonctionnalités temps réel, pas de plomberie

    Salles, accusés de réception, reconnexion, heartbeats et repli polling sont inclus. Cela représente des semaines de cas particuliers que vous n'aurez pas à découvrir en production.

  • Une seule API des deux côtés

    Le serveur et le client partagent le même modèle d'événements ; une fonctionnalité ne prend généralement que quelques lignes de chaque côté. L'intégration d'un développeur frontend se fait en quelques minutes.

  • Mise à l'échelle multi-instances

    Le Redis adapter transforme une flotte de pods en un seul serveur logique pour les salles et les diffusions, ce qui est la partie la plus difficile de tout déploiement temps réel.

Trade-offs

  • Ce n'est pas du WebSocket pur

    Socket.IO définit son propre protocole au-dessus d'Engine.IO. Un client WebSocket natif ne peut pas se connecter ; vous avez donc besoin du client Socket.IO sur chaque plateforme.

  • Les salles résident en mémoire

    L'appartenance aux salles est conservée par processus, sauf si vous utilisez un adaptateur. Sans Redis, une reconnexion sur une autre instance peut silencieusement aboutir au mauvais endroit.

  • Risque de sur-diffusion

    io.emit envoie à tous les sockets connectés. C'est à portée d'une touche et peut fuiter des données ou faire tomber un déploiement massif ; ciblez donc vos salles délibérément.

FAQ

Foire aux questions

Keep learning

Related topics from the roadmap.

$ commencer à apprendre

Prêt à apprendre Socket.IO ?

Notre tutoriel interactif vous guide à travers Socket.IO pas à pas — avec des quiz et du vrai code que vous pouvez exécuter dans le navigateur.