Uma GraphQL subscription é um stream de eventos tipado e definido pelo schema; um WebSocket é o transporte cru sobre o qual ela costuma rodar. O “vs.” do título é uma confusão de camadas que vale desfazer antes de qualquer decisão: subscriptions não são uma alternativa a WebSockets — são uma das coisas que você pode rodar sobre um, do jeito que o HTTP roda sobre TCP. A escolha real é entre um protocolo tipado que alguém já especificou e um cano cru cujo protocolo você inventa.
Principais pontos
| Pergunta | Resposta |
|---|---|
| WebSocket | Um cano de bytes persistente e full-duplex — sem semântica de mensagem inclusa |
| GraphQL subscription | Um stream de eventos definido pelo schema, tipado e validado como qualquer resposta GraphQL |
| A relação entre eles | Camadas, não rivais — subscriptions rodam sobre WebSockets (ou SSE) via um sub-protocolo |
| O sub-protocolo | graphql-ws: handshake init/ack, ids multiplexam operações, frames next/complete |
| A decisão de verdade | Protocolo tipado de prateleira vs. socket cru mais um protocolo que agora é seu |
As camadas, em código
O que uma subscription realmente é no fio — uma conversa graphql-ws dentro de um WebSocket:
// cliente → servidor, depois que o socket abre
{ "type": "connection_init", "payload": { "authToken": "…" } }
// servidor → cliente
{ "type": "connection_ack" }
// o cliente inicia uma operação — o id multiplexa esta subscription
{ "type": "subscribe", "id": "1", "payload": {
"query": "subscription { orderUpdated(status: PREPARING) { id status eta } }" } }
// o servidor transmite eventos tipados, um frame por ocorrência
{ "type": "next", "id": "1", "payload": { "data": { "orderUpdated": { "id": "o42", "status": "READY", "eta": null } } } }
// qualquer um dos lados encerra o stream
{ "type": "complete", "id": "1" }
E o que a maior parte do código de aplicação de fato escreve — uma subscription tipada com a pilha inteira gerenciada:
// 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) } Uma pilha, três camadas
A camada WebSocket (RFC 6455) promete exatamente isto: um stream persistente, full-duplex e ordenado de frames de texto ou binários, acessível de qualquer navegador por uma API pequena. Ela não diz nada sobre o que uma mensagem significa — sem correlação requisição/resposta, sem convenção de autenticação, sem sinalização de erro, sem jeito de rodar dois streams lógicos em um socket. Todo projeto de socket cru redecide tudo isso.
A camada de subscription é precisamente esse conjunto de decisões faltantes, padronizado: connection_init/connection_ack carrega a autenticação; ids por operação multiplexam muitas subscriptions em um socket; frames next entregam payloads que são respostas GraphQL comuns — tipadas pelo schema, validadas, introspectáveis, consumidas pela mesma maquinaria de cliente que queries e mutations; complete encerra um stream sem matar os vizinhos. Uma nota histórica importa na prática: um sub-protocolo mais antigo do início do ecossistema ainda está em produção, e um cliente falando um com um servidor falando o outro falha de formas silenciosas e confusas — fixe o sub-protocolo explicitamente nas duas pontas.
E porque o contrato é de mensagens, não de sockets, o transporte por baixo é trocável — a mesma semântica de subscription roda cada vez mais sobre Server-Sent Events, o que combina com o formato esmagadoramente unidirecional do tráfego de subscription e herda a tolerância a proxies e a reconexão automática do SSE. “Subscriptions vs. WebSockets” se dissolve ao toque: uma é contrato, o outro é carregador.
GraphQL subscriptions vs. WebSockets crus
| GraphQL subscriptions | WebSockets crus | |
|---|---|---|
| Camada | Protocolo + sistema de tipos sobre um transporte | O transporte em si |
| Contrato de mensagem | Definido pelo schema, validado, introspectável | O que você inventar e documentar |
| Multiplexação | Embutida — ids por operação em um socket | Por sua conta |
| Handshake de autenticação | Padronizado (payload do connection_init) | Por sua conta |
| Payloads | JSON, no formato do schema | Texto e binário, qualquer formato |
| Filtragem | Argumentos no campo da subscription | Código de servidor que você escreve |
| Overhead por evento | Envelope JSON + execução de resolver | ~2–14 bytes de framing |
| Ecossistema | Clientes GraphQL, codegen, ferramental | Bibliotecas de socket cru |
| Melhor quando | Os eventos são dados de API tipados em um app GraphQL | Binário, alta frequência ou semântica própria |
Quando sockets crus ganham das subscriptions tipadas
Os casos honestos existem, só que são mais estreitos do que o entusiasmo por socket cru sugere. Payloads binários — pedaços de áudio, protocol buffers, estado de jogo — trafegam nativamente em frames WebSocket, mas precisariam ser codificados dentro de um envelope JSON de subscription. Taxa de mensagens — a milhares de eventos por segundo por cliente, a execução de resolver e o envelope JSON por evento deixam de ser ruído; um formato de frame compacto e próprio é otimização legítima. Semântica própria — backpressure, acks do cliente, codificação de deltas, cursores retomáveis — pertence a protocolos que você projeta, e enfiá-los nos frames de subscription briga com a spec. E não ter GraphQL para começar — adotar schema, resolvers e ferramental de cliente só para ter eventos tipados é o rabo abanando o cachorro; um socket cru com formato de mensagem documentado é menor. A armadilha corre no sentido inverso também: times que escolhem sockets crus para eventos de API tipados e no formato JSON acabam escrevendo à mão multiplexação, handshake de autenticação e semântica de reconexão — um graphql-ws pior, um time incompatível por vez.
Casos de uso comuns
- Streams de pedido e status — “me avise quando este pedido mudar”: dados de API tipados, taxa baixa, o ponto ideal das subscriptions.
- Presença e comentários colaborativos — subscriptions em apps GraphQL que já são donos do schema; os eventos são só mais schema.
- Cotações financeiras e dashboards — subscriptions enquanto os payloads seguem no formato JSON; sockets crus quando a taxa de ticks exige frames compactos.
- Chat — qualquer uma das camadas funciona; o voto de minerva costuma ser se o app é GraphQL-first, já que as live queries cobrem o mesmo terreno sem fiação de eventos.
- Estado multiplayer e mídia — binário, alta frequência, sensível a latência: território de WebSocket cru, protocolo e tudo.
Você deveria usar GraphQL subscriptions ou WebSockets crus? Matriz de decisão
| Sua situação | Use |
|---|---|
| O app já fala GraphQL; os eventos são dados de API tipados | GraphQL subscriptions |
| Frames binários, ou milhares de eventos/s por cliente | WebSockets crus |
| Você precisa de semântica própria — acks, deltas, cursores, backpressure | WebSockets crus, com o protocolo documentado |
| Nenhum investimento em GraphQL, feed de eventos simples | Socket cru com um pequeno protocolo de frames — ou SSE |
| Os eventos são “os resultados desta consulta mudaram” | Uma camada de live query — sem fiação de eventos nenhuma |
| Proxies rígidos, infraestrutura só HTTP | Subscriptions sobre SSE |
| Time pequeno, sem apetite para ser dono de protocolo | Subscriptions tipadas em um backend gerenciado |
Limitações e trade-offs
- Subscriptions herdam a operação de WebSockets. Empilhar camadas acrescenta significado, não mágica: estado de conexão, roteamento com afinidade, heartbeats e um backplane de pub/sub entre servidores continuam sendo a realidade de deploy por baixo.
- Reconexão ainda perde eventos. O graphql-ws define streams, não retomada: um socket que cai significa frames perdidos, e a atualização (reconsultar e depois reinscrever) é lógica de aplicação nas duas pilhas.
- Resolução por assinante custa. Subscriptions filtradas podem executar trabalho de resolver e de permissão por evento e por assinante — um tópico quente com milhares de ouvintes multiplica isso; projete filtros no servidor e estreitos.
- Circulam dois sub-protocolos. Os protocolos legado e moderno de GraphQL sobre WebSocket são mutuamente ininteligíveis; pontas desencontradas falham em silêncio. Fixe as versões explicitamente.
- Envelopes tipados cobram throughput. Serialização JSON e validação de schema por evento são invisíveis a dezenas de eventos por segundo e dominantes a milhares — meça antes de assumir qualquer coisa.
GraphQL subscriptions e WebSockets no Back4app
O Back4app é uma plataforma open-source de Backend as a Service (BaaS) que combina banco de dados gerenciado, APIs REST e GraphQL geradas automaticamente, autenticação, armazenamento de arquivos e funções serverless com Cloud Code. O empilhamento descrito neste artigo mapeia direto na plataforma: a API GraphQL gerada automaticamente cobre as camadas de query e mutation a partir do seu schema, sem nenhum código de resolver, enquanto a camada de tempo real chega como Live Queries — eventos de subscription tipados e verificados por ACL sobre uma frota gerenciada de WebSockets, usando o protocolo aberto LiveQuery no lugar do graphql-ws e disparando por mudança no conjunto de resultados em vez de eventos conectados à mão. Você fica com a coluna do protocolo tipado da tabela comparativa — multiplexação, autenticação e tratamento de reconexão inclusos — sem operar a frota de sockets nem ser dono de uma especificação de protocolo.
Perguntas frequentes
GraphQL subscriptions e WebSockets são a mesma coisa?
Não — elas vivem em camadas diferentes. Um WebSocket é transporte: um cano de bytes persistente e full-duplex, sem opinião sobre o que trafega por ele. Uma GraphQL subscription é protocolo e contrato por cima: uma operação definida no schema cujos eventos chegam tipados, validados e no mesmo formato de qualquer outra resposta GraphQL. Comparar as duas diretamente é comparar uma estrada com uma linha de ônibus.
Qual protocolo as GraphQL subscriptions usam?
Normalmente o graphql-ws, o sub-protocolo moderno de GraphQL sobre WebSocket: o cliente abre o socket, envia connection_init, recebe connection_ack e então inicia operações com mensagens subscribe; o servidor transmite payloads next por id de subscription e qualquer um dos lados encerra com complete. Um sub-protocolo mais antigo do início do ecossistema ainda circula, e é por isso que cliente e servidor precisam concordar sobre qual dos dois falam.
GraphQL subscriptions podem rodar sobre Server-Sent Events?
Podem — o contrato de subscription é agnóstico de transporte, e SSE é um carregador legítimo, cada vez mais popular. Como o tráfego de subscription é esmagadoramente do servidor para o cliente, um stream HTTP unidirecional encaixa naturalmente, mantém proxies comuns e a semântica HTTP satisfeitos e traz reconexão automática de graça. WebSockets seguem como padrão na maior parte do ferramental, mas "subscriptions exigem WebSockets" é folclore, não fato.
Quando usar WebSockets crus em vez de GraphQL subscriptions?
Quando o tráfego deixa de parecer evento de API tipado: frames binários (áudio, estado de jogo, streams de sensores), taxas de mensagem muito altas em que validar schema e envelopar em JSON por evento custam throughput real, ou protocolos que precisam de semântica própria — cursores, deltas, acknowledgments — que brigam com o formato de subscription. Se você ainda não investiu em um schema GraphQL, o socket cru também evita importar um só por causa dos eventos.
GraphQL subscriptions escalam?
O transporte escala como qualquer frota de WebSockets — estado de conexão, roteamento com afinidade e um backplane de pub/sub entre servidores. A camada de subscription acrescenta um eixo próprio: cada evento pode ser resolvido e filtrado por assinante, então um tópico quente com muitos assinantes multiplica o trabalho dos resolvers. Plataformas gerenciadas absorvem a frota; o design do schema e a disciplina de filtro por assinante continuam sendo seus.
GraphQL subscriptions são a mesma coisa que live queries?
Primas próximas, com gatilhos diferentes. Subscriptions disparam em eventos nomeados que você conecta explicitamente — uma mutation publica, os assinantes recebem. Live queries disparam quando o conjunto de resultados de uma consulta muda — sem fiação de eventos, todo caminho de escrita coberto automaticamente. As duas costumam rodar sobre WebSockets. Uma camada de live query troca o design de eventos guiado por schema pela detecção automática de mudanças nos próprios dados.
Por que subscriptions precisam de um sub-protocolo?
Porque um WebSocket puro é só bytes ordenados. No momento em que duas partes precisam multiplexar várias subscriptions em um socket, correlacionar eventos com operações, negociar autenticação, sinalizar erros e encerrar streams com limpeza, elas precisam de framing de mensagens e regras — que todo time já inventou mal, uma versão incompatível por vez. O graphql-ws padroniza exatamente essa camada para que clientes e servidores interoperem.