Una subscription GraphQL è un flusso di eventi tipizzato e definito dallo schema; un WebSocket è il trasporto grezzo su cui di solito viaggia. Il “vs.” del titolo nasconde una confusione di livelli che conviene sciogliere prima di qualsiasi decisione: le subscription non sono un’alternativa ai WebSocket – sono una delle cose che si possono far girare sopra un WebSocket, come HTTP gira sopra TCP. La scelta vera è fra un protocollo tipizzato che qualcun altro ha specificato e un canale grezzo il cui protocollo resta da inventare.
Punti chiave
| Domanda | Risposta |
|---|---|
| WebSocket | Un canale di byte persistente e full-duplex – nessuna semantica dei messaggi inclusa |
| Subscription GraphQL | Un flusso di eventi definito dallo schema, tipizzato e validato come qualsiasi risposta GraphQL |
| La loro relazione | Livelli sovrapposti, non rivali – le subscription viaggiano su WebSocket (o SSE) tramite un sotto-protocollo |
| Il sotto-protocollo | graphql-ws: handshake init/ack, id che multiplexano le operazioni, frame next/complete |
| La decisione vera | Protocollo tipizzato pronto all’uso vs. socket grezzo + un protocollo di cui farsi carico |
I livelli, in codice
Che cos’è davvero una subscription nel traffico di rete – una conversazione graphql-ws dentro un WebSocket:
// client → server, una volta aperto il socket
{ "type": "connection_init", "payload": { "authToken": "…" } }
// server → client
{ "type": "connection_ack" }
// il client avvia un'operazione – l'id multiplexa questa subscription
{ "type": "subscribe", "id": "1", "payload": {
"query": "subscription { orderUpdated(status: PREPARING) { id status eta } }" } }
// il server trasmette eventi tipizzati, un frame per occorrenza
{ "type": "next", "id": "1", "payload": { "data": { "orderUpdated": { "id": "o42", "status": "READY", "eta": null } } } }
// uno dei due lati chiude il flusso
{ "type": "complete", "id": "1" }
E quello che scrive davvero la maggior parte del codice applicativo – una subscription tipizzata con l’intero stack gestito:
// 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) } Uno stack, tre livelli
Il livello WebSocket (RFC 6455) promette esattamente questo: un flusso persistente, full-duplex e ordinato di frame di testo o binari, raggiungibile da qualsiasi browser tramite una piccola API. Non dice nulla su che cosa un messaggio significhi – nessuna correlazione richiesta/risposta, nessuna convenzione di autenticazione, nessuna segnalazione degli errori, nessun modo di far passare due flussi logici su un solo socket. Ogni progetto su socket grezzo ridecide tutto da capo.
Il livello di subscription è precisamente quell’insieme di decisioni mancanti, standardizzato: connection_init/connection_ack trasporta l’autenticazione; gli id per operazione multiplexano molte subscription su un solo socket; i frame next consegnano payload che sono normali risposte GraphQL – tipizzate dallo schema, validate, ispezionabili, consumate dalla stessa macchina client di query e mutation; complete chiude un flusso senza uccidere i vicini. Una nota storica conta nella pratica: un sotto-protocollo più vecchio, ereditato dai primi tempi dell’ecosistema, è ancora in produzione, e un client che parla l’uno con un server che parla l’altro fallisce in modi silenziosi e sconcertanti – conviene fissare esplicitamente il sotto-protocollo a entrambe le estremità.
E poiché il contratto riguarda i messaggi, non i socket, il trasporto sottostante è intercambiabile – la stessa semantica di subscription gira sempre più spesso su Server-Sent Events, che si adatta alla forma quasi esclusivamente unidirezionale del traffico delle subscription ed eredita la compatibilità di SSE con i proxy e la riconnessione automatica. “Subscription vs. WebSocket” si dissolve al primo contatto: uno è un contratto, l’altro un vettore.
Subscription GraphQL vs. WebSocket grezzi
| Subscription GraphQL | WebSocket grezzi | |
|---|---|---|
| Livello | Protocollo + sistema di tipi sopra un trasporto | Il trasporto stesso |
| Contratto dei messaggi | Definito dallo schema, validato, ispezionabile | Quello che si inventa e si documenta |
| Multiplexing | Integrato – id per operazione su un solo socket | Da progettare in proprio |
| Handshake di autenticazione | Standardizzato (payload di connection_init) | Da progettare in proprio |
| Payload | JSON, nella forma dello schema | Testo e binario, qualsiasi formato |
| Filtri | Argomenti sul campo della subscription | Codice server da scrivere |
| Overhead per evento | Busta JSON + esecuzione dei resolver | ~2–14 byte di framing |
| Ecosistema | Client GraphQL, codegen, strumenti | Librerie di socket nudi |
| Ideale quando | Gli eventi sono dati API tipizzati in un’app GraphQL | Binario, alta frequenza o semantica personalizzata |
Quando i socket grezzi battono le subscription tipizzate
I casi onesti esistono, solo più stretti di quanto suggerisca l’entusiasmo per il socket grezzo. Payload binari – frammenti audio, protocol buffer, stato di gioco – viaggiano nativamente nei frame WebSocket ma andrebbero codificati dentro una busta JSON di subscription. Frequenza dei messaggi – a migliaia di eventi al secondo per client, l’esecuzione dei resolver e la busta JSON per evento smettono di essere rumore; un formato di frame compatto e personalizzato è un’ottimizzazione legittima. Semantica personalizzata – backpressure, ack lato client, codifica a delta, cursori ripristinabili – appartiene ai protocolli progettati in proprio, e innestarla sui frame di subscription significa lottare contro la specifica. E nessun GraphQL in partenza – adottare schema, resolver e strumenti client solo per avere eventi tipizzati significa lasciare che un dettaglio detti l’intera architettura; un socket grezzo con un formato dei messaggi documentato è più piccolo. La trappola funziona anche al contrario: i team che scelgono socket grezzi per eventi API tipizzati in forma JSON finiscono per scrivere a mano multiplexing, handshake di autenticazione e semantica di riconnessione – un graphql-ws peggiore, un team incompatibile alla volta.
Casi d’uso tipici
- Flussi di ordini e stati – “una notifica quando questo ordine cambia”: dati API tipizzati, frequenza bassa, il terreno ideale delle subscription.
- Presenza e commenti collaborativi – subscription in app GraphQL che già possiedono lo schema; gli eventi sono solo altro schema.
- Ticker finanziari e dashboard – subscription finché i payload restano in forma JSON; socket grezzi quando la cadenza dei tick esige frame compatti.
- Chat – entrambi i livelli funzionano; il voto decisivo è di solito se l’app è GraphQL-first, dato che le live query coprono lo stesso terreno senza cablaggio di eventi.
- Stato multigiocatore e media – binario, alta frequenza, critico per la latenza: territorio del WebSocket grezzo, protocollo compreso.
Subscription GraphQL o WebSocket grezzi? Matrice decisionale
| Situazione | Scegliere |
|---|---|
| L’app parla già GraphQL; gli eventi sono dati API tipizzati | Subscription GraphQL |
| Frame binari, o migliaia di eventi/s per client | WebSocket grezzi |
| Servono semantiche personalizzate – ack, delta, cursori, backpressure | WebSocket grezzi, con protocollo documentato |
| Nessun investimento in GraphQL, semplice feed di eventi | Socket grezzo con un piccolo protocollo a frame – oppure SSE |
| Gli eventi significano “i risultati di questa query sono cambiati” | Un livello di live query – nessun cablaggio di eventi |
| Proxy rigidi, infrastruttura solo HTTP | Subscription su SSE |
| Team piccolo, nessuna voglia di possedere un protocollo | Subscription tipizzate su un backend gestito |
Limiti e trade-off
- Le subscription ereditano l’esercizio dei WebSocket. Sovrapporre livelli aggiunge significato, non magia: stato delle connessioni, routing con affinità, heartbeat e backplane pub/sub fra server restano la realtà del deployment sottostante.
- La riconnessione perde comunque eventi. graphql-ws definisce flussi, non ripresa: un socket caduto significa frame persi, e il recupero (rieseguire la query, poi risottoscrivere) è logica applicativa su entrambi gli stack.
- La risoluzione per sottoscrittore costa. Le subscription filtrate possono eseguire resolver e verifiche dei permessi per evento e per sottoscrittore – un topic caldo con migliaia di ascoltatori le moltiplica; conviene progettare filtri lato server, e stretti.
- Circolano due sotto-protocolli. I protocolli GraphQL-over-WebSocket legacy e moderno sono reciprocamente incomprensibili; estremità disallineate falliscono in silenzio. Le versioni vanno fissate esplicitamente.
- Le buste tipizzate tassano il throughput. Serializzazione JSON e validazione dello schema per evento sono invisibili a decine di eventi al secondo e dominanti a migliaia – misurare prima di dare per scontato l’uno o l’altro esito.
GraphQL Subscriptions e WebSocket su Back4app
Back4app è una piattaforma open-source di Backend as a Service (BaaS) che combina database gestito, API REST e GraphQL generate automaticamente, autenticazione, archiviazione di file e funzioni serverless con Cloud Code. I livelli descritti in questo articolo si ritrovano tali e quali sulla piattaforma: l’API GraphQL generata automaticamente copre i livelli query e mutation a partire dallo schema senza una riga di resolver, mentre il livello in tempo reale arriva sotto forma di Live Queries – eventi di sottoscrizione tipizzati e verificati dalle ACL su una flotta di WebSocket gestita, che usano il protocollo aperto LiveQuery al posto di graphql-ws e scattano sui cambiamenti dell’insieme di risultati invece che su eventi cablati a mano. Si ottiene la colonna “protocollo tipizzato” della tabella comparativa – multiplexing, autenticazione e gestione della riconnessione compresi – senza gestire la flotta di socket né possedere la specifica di un protocollo.
Domande frequenti
Le subscription GraphQL sono la stessa cosa dei WebSocket?
No – vivono in livelli diversi. Un WebSocket è un trasporto: un canale di byte persistente e full-duplex, senza alcuna opinione su ciò che lo attraversa. Una subscription GraphQL è un protocollo e un contratto posati sopra: un'operazione definita nello schema i cui eventi arrivano tipizzati, validati e con la stessa forma di qualsiasi altra risposta GraphQL. Confrontarli direttamente è come confrontare una strada con una linea di autobus.
Quale protocollo usano le subscription GraphQL?
Nella maggior parte dei casi graphql-ws, il sotto-protocollo moderno di GraphQL su WebSocket: il client apre un socket, invia connection_init, riceve connection_ack, poi avvia le operazioni con messaggi subscribe; il server trasmette payload next per id di subscription e uno dei due lati chiude con complete. Un sotto-protocollo più vecchio, ereditato dai primi tempi dell'ecosistema, circola ancora – per questo client e server devono accordarsi su quale dei due parlano.
Le subscription GraphQL possono viaggiare su Server-Sent Events?
Sì – il contratto di subscription è indipendente dal trasporto, e SSE è un vettore legittimo e sempre più diffuso. Poiché il traffico delle subscription va quasi esclusivamente dal server al client, un flusso HTTP unidirezionale si adatta in modo naturale, va d'accordo con i proxy ordinari e con la semantica HTTP, e porta con sé la riconnessione automatica senza costi. I WebSocket restano la scelta predefinita della maggior parte degli strumenti, ma "le subscription richiedono i WebSocket" è folklore, non un fatto.
Quando usare WebSocket grezzi invece delle subscription GraphQL?
Quando il traffico smette di assomigliare a eventi API tipizzati: frame binari (audio, stato di gioco, flussi di sensori), frequenze di messaggi molto alte in cui la validazione dello schema e la busta JSON per evento costano throughput reale, oppure protocolli che richiedono una semantica propria – cursori, delta, ack – in conflitto con la forma della subscription. Se non c'è già un investimento in uno schema GraphQL, un socket grezzo evita anche di importarne uno solo per gli eventi.
Le subscription GraphQL scalano?
Il trasporto scala come qualsiasi flotta di WebSocket – stato delle connessioni, routing con affinità (sticky) e backplane pub/sub fra server. Il livello di subscription aggiunge un asse proprio: ogni evento può essere risolto e filtrato per sottoscrittore, quindi un topic caldo con molti sottoscrittori moltiplica il lavoro dei resolver. Le piattaforme gestite assorbono la flotta; la progettazione dello schema e la disciplina nei filtri per sottoscrittore restano in mano a chi sviluppa.
Le subscription GraphQL sono la stessa cosa delle live query?
Parenti strette, con trigger diversi. Le subscription scattano su eventi nominati cablati esplicitamente – una mutation pubblica su un topic, i sottoscrittori ricevono. Le live query scattano sui cambiamenti dell'insieme di risultati di una query – nessun cablaggio di eventi, ogni percorso di scrittura coperto automaticamente. Entrambe viaggiano di solito su WebSocket. Un livello di live query scambia la progettazione degli eventi schema-first con il rilevamento automatico dei cambiamenti sui dati stessi.
Perché le subscription hanno bisogno di un sotto-protocollo?
Perché un WebSocket nudo è solo una sequenza ordinata di byte. Nel momento in cui due parti devono multiplexare più subscription su un solo socket, correlare gli eventi alle operazioni, negoziare l'autenticazione, segnalare gli errori e chiudere i flussi in modo pulito, servono un framing dei messaggi e delle regole – che ogni team un tempo ha inventato male, ciascuno in una versione incompatibile. graphql-ws standardizza esattamente quel livello perché client e server possano interoperare.