Une subscription GraphQL est un flux d’événements typé et défini par le schéma ; un WebSocket est le transport brut qui la porte le plus souvent. Le « vs. » du titre cache une confusion de couches qu’il vaut mieux démêler avant toute décision : les subscriptions ne sont pas une alternative aux WebSockets — elles sont l’une des choses que l’on peut faire tourner par-dessus un WebSocket, comme HTTP tourne par-dessus TCP. Le vrai choix se situe entre un protocole typé que quelqu’un d’autre a spécifié et un tuyau brut dont vous inventez le protocole.
Points clés
| Question | Réponse |
|---|---|
| WebSocket | Un tuyau d’octets persistant et full-duplex — aucune sémantique de message incluse |
| Subscription GraphQL | Un flux d’événements défini par le schéma, typé et validé comme toute réponse GraphQL |
| Leur relation | Des couches, pas des rivales — les subscriptions passent par des WebSockets (ou SSE) via un sous-protocole |
| Le sous-protocole | graphql-ws : handshake init/ack, des ids qui multiplexent les opérations, frames next/complete |
| La vraie décision | Protocole typé prêt à l’emploi vs. socket brut + un protocole qui est désormais le vôtre |
Les couches, en code
Ce qu’est réellement une subscription sur le fil — une conversation graphql-ws à l’intérieur d’un WebSocket :
// client → serveur, une fois le socket ouvert
{ "type": "connection_init", "payload": { "authToken": "…" } }
// serveur → client
{ "type": "connection_ack" }
// le client lance une opération — l'id multiplexe cette subscription
{ "type": "subscribe", "id": "1", "payload": {
"query": "subscription { orderUpdated(status: PREPARING) { id status eta } }" } }
// le serveur diffuse des événements typés, une frame par occurrence
{ "type": "next", "id": "1", "payload": { "data": { "orderUpdated": { "id": "o42", "status": "READY", "eta": null } } } }
// l'un ou l'autre côté termine le flux
{ "type": "complete", "id": "1" }
Et ce que la plupart du code applicatif écrit en pratique — une subscription typée, toute la pile étant gérée :
// JavaScript — Back4app JS SDK
// A typed subscription over a managed WebSocket fleet: the protocol,
// reconnects, and fan-out are the platform's problem, not yours
const orders = new Parse.Query('Order');
orders.equalTo('status', 'preparing');
const sub = await orders.subscribe();
sub.on('create', (o) => addCard(o)); // typed event, full object
sub.on('update', (o) => refreshCard(o));
sub.on('leave', (o) => removeCard(o)); // edited out of the result set
sub.on('close', () => showOfflineBadge()); // socket lifecycle surfaced // Flutter / Dart — Back4app Flutter SDK
// A typed subscription over a managed WebSocket fleet: the protocol,
// reconnects, and fan-out are the platform's problem, not yours
final liveQuery = LiveQuery();
final orders = QueryBuilder<ParseObject>(ParseObject('Order'))
..whereEqualTo('status', 'preparing');
final sub = await liveQuery.client.subscribe(orders);
sub.on(LiveQueryEvent.create, (o) => addCard(o)); // typed event
sub.on(LiveQueryEvent.update, (o) => refreshCard(o));
sub.on(LiveQueryEvent.leave, (o) => removeCard(o)); // left the set // iOS / Swift — Back4app Swift SDK
// A typed subscription over a managed WebSocket fleet: the protocol,
// reconnects, and fan-out are the platform's problem, not yours
let orders = Order.query("status" == "preparing")
let subscription = orders.subscribeCallback
subscription?.handleEvent { _, event in
switch event {
case .created(let o): addCard(o) // typed event, full object
case .updated(let o): refreshCard(o)
case .left(let o): removeCard(o) // edited out of the result set
default: break
}
} // Android / Kotlin — Back4app Android SDK
// A typed subscription over a managed WebSocket fleet: the protocol,
// reconnects, and fan-out are the platform's problem, not yours
val client = ParseLiveQueryClient.Factory.getClient()
val orders = ParseQuery.getQuery<ParseObject>("Order")
orders.whereEqualTo("status", "preparing")
val sub = client.subscribe(orders)
sub.handleEvent(SubscriptionHandling.Event.CREATE) { _, o -> addCard(o) }
sub.handleEvent(SubscriptionHandling.Event.UPDATE) { _, o -> refreshCard(o) }
sub.handleEvent(SubscriptionHandling.Event.LEAVE) { _, o -> removeCard(o) } Une pile, trois couches
La couche WebSocket (RFC 6455) promet exactement ceci : un flux persistant, full-duplex et ordonné de frames texte ou binaires, accessible depuis n’importe quel navigateur grâce à une petite API. Elle ne dit rien de ce qu’un message signifie — pas de corrélation requête/réponse, pas de convention d’authentification, pas de signalement d’erreurs, aucun moyen de faire circuler deux flux logiques sur un même socket. Chaque projet sur socket brut redécide tout cela.
La couche de subscription est précisément cet ensemble de décisions manquantes, standardisé : connection_init/connection_ack transporte l’authentification ; des id par opération multiplexent de nombreuses subscriptions sur un seul socket ; les frames next livrent des payloads qui sont des réponses GraphQL ordinaires — typées par le schéma, validées, introspectables, consommées par la même mécanique client que les queries et les mutations ; complete met fin à un flux sans tuer ses voisins. Une note historique compte en pratique : un sous-protocole plus ancien, hérité des débuts de l’écosystème, est toujours déployé, et un client qui parle l’un à un serveur qui parle l’autre échoue de façon déroutante et silencieuse — fixez explicitement le sous-protocole aux deux extrémités.
Et comme le contrat porte sur des messages, pas sur des sockets, le transport sous-jacent est interchangeable — la même sémantique de subscription tourne de plus en plus sur Server-Sent Events, qui épouse la forme massivement unidirectionnelle du trafic des subscriptions et hérite de la compatibilité de SSE avec les proxies et de sa reconnexion automatique. « Subscriptions vs. WebSockets » se dissout au premier contact : l’un est un contrat, l’autre un porteur.
Subscriptions GraphQL vs. WebSockets bruts
| Subscriptions GraphQL | WebSockets bruts | |
|---|---|---|
| Couche | Protocole + système de types au-dessus d’un transport | Le transport lui-même |
| Contrat de message | Défini par le schéma, validé, introspectable | Ce que vous inventez et documentez |
| Multiplexage | Intégré — ids par opération sur un seul socket | À concevoir vous-même |
| Handshake d’authentification | Standardisé (payload de connection_init) | À concevoir vous-même |
| Payloads | JSON, façonnés par le schéma | Texte et binaire, tout format |
| Filtrage | Arguments sur le champ de subscription | Code serveur que vous écrivez |
| Overhead par événement | Enveloppe JSON + exécution des resolvers | ~2–14 octets de framing |
| Écosystème | Clients GraphQL, génération de code, outillage | Bibliothèques de sockets nus |
| Idéal quand | Les événements sont des données d’API typées dans une app GraphQL | Binaire, haute fréquence ou sémantique propre |
Quand les sockets bruts battent les subscriptions typées
Les cas honnêtes existent, simplement plus étroits que ne le laisse croire l’enthousiasme pour le socket brut. Les payloads binaires — morceaux d’audio, protocol buffers, état de jeu — voyagent nativement dans des frames WebSocket mais devraient être encodés dans une enveloppe JSON de subscription. Le débit de messages — à des milliers d’événements par seconde par client, l’exécution des resolvers et l’enveloppe JSON par événement cessent d’être du bruit ; un format de frame compact et maison est une optimisation légitime. La sémantique propre — backpressure, acquittements côté client, encodage par deltas, curseurs de reprise — appartient aux protocoles que vous concevez, et la greffer sur des frames de subscription revient à lutter contre la spécification. Et l’absence de GraphQL au départ — adopter un schéma, des resolvers et un outillage client uniquement pour obtenir des événements typés, c’est la queue qui remue le chien ; un socket brut avec un format de message documenté est plus léger. Le piège joue aussi dans l’autre sens : les équipes qui choisissent des sockets bruts pour des événements d’API typés au format JSON finissent par écrire à la main le multiplexage, les handshakes d’authentification et la sémantique de reconnexion — un graphql-ws en moins bien, une équipe incompatible à la fois.
Cas d’usage courants
- Flux de commandes et de statuts — « prévenez-moi quand cette commande change » : données d’API typées, faible débit, le terrain idéal des subscriptions.
- Présence et commentaires collaboratifs — des subscriptions dans des apps GraphQL qui possèdent déjà le schéma ; les événements ne sont qu’un morceau de schéma de plus.
- Cotations financières et dashboards — des subscriptions tant que les payloads restent au format JSON ; des sockets bruts quand la cadence des ticks exige des frames compactes.
- Chat — les deux couches conviennent ; le vote décisif porte généralement sur le caractère GraphQL-first de l’app, puisque les live queries couvrent le même terrain sans câblage d’événements.
- État multijoueur et médias — binaire, haute fréquence, critique en latence : territoire du WebSocket brut, protocole compris.
Devriez-vous utiliser des subscriptions GraphQL ou des WebSockets bruts ? Matrice de décision
| Votre situation | Choisissez |
|---|---|
| L’app parle déjà GraphQL ; les événements sont des données d’API typées | Subscriptions GraphQL |
| Frames binaires, ou des milliers d’événements/s par client | WebSockets bruts |
| Vous avez besoin d’une sémantique propre — acquittements, deltas, curseurs, backpressure | WebSockets bruts, protocole documenté |
| Aucun investissement GraphQL, simple flux d’événements | Socket brut avec un petit protocole de frames — ou SSE |
| Les événements signifient « les résultats de cette requête ont changé » | Une couche de live queries — aucun câblage d’événements |
| Proxies stricts, infrastructure uniquement HTTP | Subscriptions sur SSE |
| Petite équipe, aucune envie de posséder un protocole | Subscriptions typées sur un backend géré |
Limites et trade-offs
- Les subscriptions héritent de l’exploitation des WebSockets. Superposer des couches ajoute du sens, pas de la magie : état de connexion, routage avec affinité, heartbeats et backplane pub/sub entre serveurs restent la réalité du déploiement en dessous.
- La reconnexion perd toujours des événements. graphql-ws définit des flux, pas de reprise : un socket coupé signifie des frames manquées, et le rattrapage (relancer la requête, puis se réabonner) relève de la logique applicative sur l’une comme l’autre pile.
- La résolution par abonné a un coût. Les subscriptions filtrées peuvent exécuter resolvers et vérifications de permissions par événement et par abonné — un topic chaud aux milliers d’auditeurs multiplie ce travail ; concevez des filtres côté serveur, et étroits.
- Deux sous-protocoles circulent. Les protocoles GraphQL sur WebSocket legacy et moderne sont mutuellement inintelligibles ; des extrémités désaccordées échouent en silence. Fixez les versions explicitement.
- Les enveloppes typées pèsent sur le débit. La sérialisation JSON et la validation de schéma par événement sont invisibles à quelques dizaines d’événements par seconde et dominantes à plusieurs milliers — mesurez avant de présumer dans un sens ou dans l’autre.
Subscriptions GraphQL et WebSockets sur Back4app
Back4app est une plateforme open-source de Backend as a Service (BaaS) qui combine une base de données gérée, des API REST et GraphQL générées automatiquement, l’authentification, le stockage de fichiers et des fonctions serverless avec Cloud Code. Les couches décrites dans cet article se retrouvent telles quelles sur la plateforme : l’API GraphQL générée automatiquement couvre les couches query et mutation à partir de votre schéma sans une ligne de resolver, tandis que la couche temps réel est livrée sous forme de Live Queries — des événements d’abonnement typés et vérifiés par ACL sur une flotte de WebSockets gérée, qui utilisent le protocole ouvert LiveQuery à la place de graphql-ws et se déclenchent sur les changements de l’ensemble de résultats plutôt que sur des événements câblés à la main. Vous obtenez la colonne « protocole typé » du tableau comparatif — multiplexage, authentification et gestion de la reconnexion compris — sans exploiter la flotte de sockets ni posséder la spécification d’un protocole.
Questions fréquentes
Les subscriptions GraphQL sont-elles la même chose que les WebSockets ?
Non — elles vivent dans des couches différentes. Un WebSocket est un transport : un tuyau d'octets persistant et full-duplex, sans avis sur ce qui le traverse. Une subscription GraphQL est un protocole et un contrat posés par-dessus : une opération définie dans le schéma dont les événements arrivent typés, validés et façonnés exactement comme n'importe quelle autre réponse GraphQL. Les comparer directement revient à comparer une route et une ligne de bus.
Quel protocole les subscriptions GraphQL utilisent-elles ?
Le plus souvent graphql-ws, le sous-protocole moderne de GraphQL sur WebSocket : le client ouvre un socket, envoie connection_init, reçoit connection_ack, puis lance des opérations avec des messages subscribe ; le serveur diffuse des payloads next par id de subscription, et l'un ou l'autre côté termine avec complete. Un sous-protocole plus ancien, hérité des débuts de l'écosystème, circule encore — c'est pourquoi client et serveur doivent s'accorder sur celui qu'ils parlent.
Les subscriptions GraphQL peuvent-elles passer par Server-Sent Events ?
Oui — le contrat de subscription est indépendant du transport, et SSE est un porteur légitime de plus en plus répandu. Comme le trafic des subscriptions va massivement du serveur vers le client, un flux HTTP unidirectionnel s'y prête naturellement, ménage les proxies ordinaires et la sémantique HTTP, et apporte la reconnexion automatique gratuitement. Les WebSockets restent le choix par défaut de la plupart des outils, mais « les subscriptions exigent des WebSockets » relève du folklore, pas du fait.
Quand utiliser des WebSockets bruts plutôt que des subscriptions GraphQL ?
Quand le trafic cesse de ressembler à des événements d'API typés : frames binaires (audio, état de jeu, flux de capteurs), débits de messages très élevés où la validation du schéma et l'enveloppe JSON par événement coûtent un vrai débit, ou protocoles exigeant une sémantique propre — curseurs, deltas, acquittements — qui se heurte à la forme d'une subscription. Si vous n'avez pas encore investi dans un schéma GraphQL, un socket brut évite aussi d'en importer un uniquement pour des événements.
Les subscriptions GraphQL passent-elles à l'échelle ?
Le transport se met à l'échelle comme n'importe quelle flotte de WebSockets — état de connexion, routage avec affinité (sticky) et backplane pub/sub entre serveurs. La couche de subscription ajoute son propre axe : chaque événement peut être résolu et filtré par abonné, si bien qu'un topic chaud aux nombreux abonnés multiplie le travail des resolvers. Les plateformes gérées absorbent la flotte ; la conception du schéma et la discipline de filtrage par abonné restent à votre charge.
Les subscriptions GraphQL sont-elles la même chose que les live queries ?
Des cousines proches aux déclencheurs différents. Les subscriptions se déclenchent sur des événements nommés que vous câblez explicitement — une mutation publie sur un topic, les abonnés reçoivent. Les live queries se déclenchent quand l'ensemble de résultats d'une requête change — aucun câblage d'événements, chaque chemin d'écriture couvert automatiquement. Les deux passent généralement par des WebSockets. Une couche de live queries échange la conception d'événements schema-first contre la détection automatique des changements sur les données elles-mêmes.
Pourquoi les subscriptions ont-elles besoin d'un sous-protocole ?
Parce qu'un WebSocket nu n'est qu'une suite ordonnée d'octets. Dès que deux parties doivent multiplexer plusieurs subscriptions sur un socket, corréler les événements aux opérations, négocier l'authentification, signaler les erreurs et clore proprement les flux, il leur faut un framing de messages et des règles — que chaque équipe a jadis mal inventés, chacune dans sa version incompatible. graphql-ws standardise exactement cette couche pour que clients et serveurs puissent interopérer.