WebSocket, SSE ou polling : quel temps réel choisir ?
Trois façons de pousser des données du serveur vers le navigateur, avec le code de chacune, leurs limites en production et le bon choix selon le cas.
HTTP a été conçu pour une conversation simple : le navigateur demande, le serveur répond, fin de l'échange. Le serveur ne peut jamais prendre l'initiative. Pour une page statique, c'est parfait. Pour un chat, une notification, un tableau de bord de suivi ou une barre de progression, c'est un problème : le serveur a une information nouvelle, et aucun moyen de la transmettre.
Trois techniques contournent cette limite. Le polling fait demander le navigateur à intervalles réguliers. Les Server-Sent Events (SSE) gardent une réponse HTTP ouverte dans laquelle le serveur écrit au fil de l'eau. Les WebSockets ouvrent un canal permanent et bidirectionnel entre les deux.
Chacune a sa place. Le piège classique est de prendre WebSocket par réflexe pour un besoin que SSE couvre en dix lignes.
Polling : demander régulièrement
Le principe le plus simple : le navigateur interroge le serveur toutes les N secondes.
async function verifierStatut() {
const res = await fetch('/api/commandes/42/statut')
const { statut } = await res.json()
document.querySelector('#statut').textContent = statut
}
verifierStatut()
setInterval(verifierStatut, 10_000)Côté serveur, rien de spécial : une route HTTP classique.
Avantages : aucune infrastructure particulière, fonctionne partout, derrière n'importe quel proxy ou CDN, et se met en cache.
Inconvénients : la majorité des requêtes reviennent sans nouveauté, et la latence est au pire égale à l'intervalle. Avec 10 000 utilisateurs qui interrogent toutes les 5 secondes, c'est 2 000 requêtes par seconde pour, la plupart du temps, ne rien apprendre.
Une variante, le long polling, garde la requête ouverte côté serveur jusqu'à ce qu'il y ait du nouveau (ou un délai maximal), puis le client relance immédiatement. Moins de requêtes inutiles, mais une gestion serveur plus délicate. Aujourd'hui, SSE fait la même chose en mieux.
Server-Sent Events : le serveur qui parle
SSE est un standard du navigateur souvent oublié. Le client ouvre une connexion HTTP normale, et le serveur ne la ferme jamais : il y écrit des événements au format texte, quand il veut.
import http from 'node:http'
http.createServer((req, res) => {
if (req.url !== '/events') {
res.writeHead(404).end()
return
}
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
})
let id = 0
const timer = setInterval(() => {
id++
res.write(`id: ${id}\n`)
res.write(`event: progression\n`)
res.write(`data: ${JSON.stringify({ pourcent: id * 10 })}\n\n`)
}, 1000)
req.on('close', () => clearInterval(timer))
}).listen(3000)const source = new EventSource('/events')
source.addEventListener('progression', (e) => {
const { pourcent } = JSON.parse(e.data)
document.querySelector('progress').value = pourcent
})Le format est minimaliste : des lignes champ: valeur, et une ligne vide pour terminer chaque événement.
Ce que SSE offre gratuitement :
- Reconnexion automatique. Si la connexion tombe,
EventSourcese reconnecte seul, sans une ligne de code. - Reprise. À la reconnexion, le navigateur renvoie l'en-tête
Last-Event-IDavec le dernieridreçu. Le serveur peut renvoyer les événements manqués. - HTTP standard. Authentification par cookie, compression, outils de débogage : tout fonctionne comme pour une requête normale.
Limites : la communication va dans un seul sens, du serveur vers le client. Pour envoyer quelque chose au serveur, le client fait une requête fetch classique, ce qui suffit dans la grande majorité des cas. Les données sont du texte (on envoie du JSON). Et en HTTP/1.1, un navigateur limite à six le nombre de connexions par domaine, onglets compris. Avec HTTP/2, cette limite disparaît en pratique, car toutes les connexions partagent un seul canal.
C'est exactement la technique qu'utilisent les interfaces de chat avec un LLM pour afficher la réponse mot par mot.
WebSocket : la conversation dans les deux sens
WebSocket commence par une requête HTTP, puis demande au serveur de « changer de protocole » (Upgrade: websocket). Une fois accepté, la connexion devient un canal TCP permanent dans lequel client et serveur s'envoient des messages, dans les deux sens, à tout moment, en texte ou en binaire.
npm install wsimport { WebSocketServer } from 'ws'
const wss = new WebSocketServer({ port: 8080 })
wss.on('connection', (socket) => {
socket.on('message', (data) => {
const message = data.toString()
// Diffuser à tous les clients connectés
for (const client of wss.clients) {
if (client.readyState === client.OPEN) client.send(message)
}
})
})const socket = new WebSocket('wss://chat.mondomaine.fr')
socket.addEventListener('message', (e) => {
afficherMessage(JSON.parse(e.data))
})
function envoyer(texte) {
socket.send(JSON.stringify({ texte, envoyeLe: Date.now() }))
}Avantages : latence minimale, bidirectionnel, binaire possible, très peu de surcoût par message. Indispensable pour un chat, un jeu multijoueur, un éditeur collaboratif ou un outil qui reçoit et envoie en continu.
Inconvénients : tout ce que SSE offrait gratuitement est à reconstruire. Pas de reconnexion automatique, pas de reprise des messages manqués, pas de mécanisme intégré pour détecter une connexion morte (il faut des « ping/pong »). L'authentification est moins directe. Et chaque connexion ouverte occupe de la mémoire sur le serveur pendant toute sa durée.
Des bibliothèques comme Socket.IO ajoutent reconnexion, salons et repli sur le polling, au prix d'un protocole propriétaire : un client Socket.IO ne parle qu'à un serveur Socket.IO.
Le comparatif
| Critère | Polling | SSE | WebSocket |
|---|---|---|---|
| Direction | Client → serveur (répété) | Serveur → client | Les deux |
| Latence | Égale à l'intervalle | Immédiate | Immédiate |
| Reconnexion automatique | Sans objet | Oui, native | Non, à coder |
| Format | Libre | Texte | Texte ou binaire |
| Passe par les proxies et CDN | Toujours | Oui, avec réglages | Oui, avec réglages |
| Coût serveur | Requêtes répétées | Une connexion par client | Une connexion par client |
| Complexité | Très faible | Faible | Moyenne à élevée |
Quel choix selon le cas
| Cas d'usage | Choix |
|---|---|
| Statut d'une commande qui change toutes les quelques minutes | Polling |
| Notifications, fil d'activité, alertes | SSE |
| Barre de progression d'un traitement long | SSE |
| Réponse d'un LLM affichée en streaming | SSE |
| Tableau de bord de monitoring en direct | SSE |
| Chat, messagerie | WebSocket |
| Jeu multijoueur, curseurs collaboratifs | WebSocket |
| Édition collaborative d'un document | WebSocket |
| Audio ou vidéo en direct | Ni l'un ni l'autre : WebRTC |
Ma règle : SSE par défaut dès que seul le serveur a des choses à pousser, WebSocket seulement quand le client envoie lui aussi un flux continu de messages. Pour la vidéo et la voix, aucune des trois ne convient : c'est le terrain de WebRTC, comparé à RTMP dans RTMP vs WebRTC.
En production : les pièges
Le reverse proxy
Nginx coupe par défaut les deux mécanismes. Pour WebSocket, il faut transmettre explicitement la demande de changement de protocole. Pour SSE, il faut désactiver la mise en mémoire tampon, sinon les événements arrivent par paquets.
location /ws/ {
proxy_pass http://localhost:8080;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 1h;
}
location /events {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_buffering off;
proxy_cache off;
proxy_read_timeout 1h;
}Le reste de la configuration est détaillé dans le guide d'installation de Nginx. Caddy gère WebSocket et SSE sans réglage particulier.
Plusieurs serveurs
Avec une seule instance, le serveur connaît tous les clients connectés. Dès qu'on lance deux instances derrière un répartiteur de charge, un message reçu par l'instance A doit atteindre des clients connectés à l'instance B. La solution classique est un canal de publication partagé : chaque instance publie les événements dans Redis et s'abonne aux autres. Le fonctionnement de Pub/Sub est présenté dans Redis pour le cache, les sessions et les files d'attente.
Les limites de l'hébergement
Les plateformes serverless facturent ou limitent souvent la durée d'exécution d'une fonction. Une connexion SSE ou WebSocket ouverte pendant une heure occupe une fonction pendant une heure. Vérifiez la documentation de l'hébergeur avant de choisir : un serveur classique ou un service dédié au temps réel est parfois plus adapté.
Pour aller plus loin
Un chat complet en WebSocket demande bien plus que les exemples ci-dessus : identification des utilisateurs, messages privés, groupes, historique, interface. La formation WebSocket construit tout ça pas à pas, du protocole jusqu'à l'interface client.
WebSocket, SSE ou polling ? Le polling pour les mises à jour rares où quelques secondes de retard ne gênent personne. SSE pour tout ce que le serveur pousse vers le navigateur, ce qui couvre la majorité des besoins réels. WebSocket quand les deux côtés parlent en continu. Commencer par la solution la plus simple qui répond au besoin, c'est aussi s'éviter une infrastructure qu'on devra maintenir pendant des années.