Una GraphQL subscription es un stream de eventos tipado y definido por el schema; un WebSocket es el transporte crudo sobre el que suele viajar. El “vs.” del título es una confusión de capas que vale la pena deshacer antes de tomar cualquier decisión: las subscriptions no son una alternativa a los WebSockets — son una de las cosas que puedes correr sobre uno, igual que HTTP corre sobre TCP. La elección real es entre un protocolo tipado que alguien ya especificó y una tubería cruda cuyo protocolo inventas tú.
Puntos clave
| Pregunta | Respuesta |
|---|---|
| WebSocket | Una tubería de bytes persistente y full-duplex — sin semántica de mensajes incluida |
| GraphQL subscription | Un stream de eventos definido por el schema, tipado y validado como cualquier respuesta GraphQL |
| Su relación | Capas, no rivales — las subscriptions viajan sobre WebSockets (o SSE) vía un sub-protocolo |
| El sub-protocolo | graphql-ws: handshake init/ack, ids que multiplexan operaciones, frames next/complete |
| La decisión real | Protocolo tipado de estante vs. socket crudo más un protocolo que ahora es tuyo |
Las capas, en código
Lo que una subscription realmente es en el cable — una conversación graphql-ws dentro de un WebSocket:
// cliente → servidor, después de que el socket abre
{ "type": "connection_init", "payload": { "authToken": "…" } }
// servidor → cliente
{ "type": "connection_ack" }
// el cliente inicia una operación — el id multiplexa esta subscription
{ "type": "subscribe", "id": "1", "payload": {
"query": "subscription { orderUpdated(status: PREPARING) { id status eta } }" } }
// el servidor transmite eventos tipados, un frame por ocurrencia
{ "type": "next", "id": "1", "payload": { "data": { "orderUpdated": { "id": "o42", "status": "READY", "eta": null } } } }
// cualquiera de los dos lados cierra el stream
{ "type": "complete", "id": "1" }
Y lo que la mayor parte del código de aplicación escribe en realidad — una subscription tipada con toda la pila gestionada:
// 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) } Una pila, tres capas
La capa WebSocket (RFC 6455) promete exactamente esto: un stream persistente, full-duplex y ordenado de frames de texto o binarios, alcanzable desde cualquier navegador mediante una API pequeña. No dice nada sobre lo que un mensaje significa — sin correlación solicitud/respuesta, sin convención de autenticación, sin señalización de errores, sin manera de correr dos streams lógicos por un socket. Cada proyecto de socket crudo vuelve a decidir todo eso.
La capa de subscription es precisamente ese conjunto de decisiones faltantes, estandarizado: connection_init/connection_ack lleva la autenticación; los id por operación multiplexan muchas subscriptions en un socket; los frames next entregan payloads que son respuestas GraphQL ordinarias — tipadas por el schema, validadas, introspectables, consumidas por la misma maquinaria de cliente que las queries y las mutations; complete cierra un stream sin matar a sus vecinos. Una nota histórica importa en la práctica: un sub-protocolo más antiguo de los inicios del ecosistema sigue desplegado, y un cliente que habla uno con un servidor que habla el otro falla de formas confusamente silenciosas — fija el sub-protocolo explícitamente en ambos extremos.
Y como el contrato es de mensajes, no de sockets, el transporte de abajo es intercambiable — la misma semántica de subscription corre cada vez más sobre Server-Sent Events, que encaja con la forma abrumadoramente unidireccional del tráfico de subscriptions y hereda la tolerancia a proxies y la reconexión automática de SSE. “Subscriptions vs. WebSockets” se disuelve al tocarlo: una es contrato, el otro es portador.
GraphQL subscriptions vs. WebSockets crudos
| GraphQL subscriptions | WebSockets crudos | |
|---|---|---|
| Capa | Protocolo + sistema de tipos sobre un transporte | El transporte en sí |
| Contrato de mensajes | Definido por el schema, validado, introspectable | Lo que inventes y documentes |
| Multiplexación | Incluida — ids por operación en un socket | Tuya de diseñar |
| Handshake de autenticación | Estandarizado (payload de connection_init) | Tuyo de diseñar |
| Payloads | JSON, con la forma del schema | Texto y binario, cualquier formato |
| Filtrado | Argumentos en el campo de la subscription | Código de servidor que escribes tú |
| Overhead por evento | Envelope JSON + ejecución de resolver | ~2–14 bytes de framing |
| Ecosistema | Clientes GraphQL, codegen, tooling | Bibliotecas de socket pelado |
| Mejor cuando | Los eventos son datos de API tipados en una app GraphQL | Binario, alta frecuencia o semántica propia |
Cuándo los sockets crudos les ganan a las subscriptions tipadas
Los casos honestos existen, solo que son más estrechos de lo que sugiere el entusiasmo por el socket crudo. Payloads binarios — trozos de audio, protocol buffers, estado de juego — viajan de forma nativa en frames WebSocket, pero necesitarían codificarse dentro de un envelope JSON de subscription. Tasa de mensajes — a miles de eventos por segundo por cliente, la ejecución de resolvers y el envelope JSON por evento dejan de ser ruido; un formato de frame compacto y propio es una optimización legítima. Semántica propia — backpressure, acks del cliente, codificación de deltas, cursores reanudables — pertenece a protocolos que diseñas tú, y atornillarla a los frames de subscription pelea con la spec. Y no tener GraphQL para empezar — adoptar schema, resolvers y tooling de cliente solo para tener eventos tipados es la cola moviendo al perro; un socket crudo con un formato de mensajes documentado es más pequeño. La trampa corre también en sentido contrario: los equipos que eligen sockets crudos para eventos de API tipados con forma JSON terminan escribiendo a mano multiplexación, handshakes de autenticación y semántica de reconexión — un graphql-ws peor, un equipo incompatible a la vez.
Casos de uso comunes
- Streams de pedidos y estados — “avísame cuando este pedido cambie”: datos de API tipados, tasa baja, el punto dulce de las subscriptions.
- Presencia y comentarios colaborativos — subscriptions en apps GraphQL que ya son dueñas del schema; los eventos son solo más schema.
- Cotizaciones financieras y dashboards — subscriptions mientras los payloads sigan con forma JSON; sockets crudos cuando la tasa de ticks exige frames compactos.
- Chat — cualquiera de las dos capas funciona; el voto decisivo suele ser si la app es GraphQL-first, ya que las live queries cubren el mismo terreno sin cableado de eventos.
- Estado multijugador y media — binario, alta frecuencia, crítico en latencia: territorio de WebSocket crudo, con protocolo y todo.
¿Deberías usar GraphQL subscriptions o WebSockets crudos? Matriz de decisión
| Tu situación | Usa |
|---|---|
| La app ya habla GraphQL; los eventos son datos de API tipados | GraphQL subscriptions |
| Frames binarios, o miles de eventos/s por cliente | WebSockets crudos |
| Necesitas semántica propia — acks, deltas, cursores, backpressure | WebSockets crudos, con el protocolo documentado |
| Sin inversión en GraphQL, feed de eventos simple | Socket crudo con un pequeño protocolo de frames — o SSE |
| Los eventos son “los resultados de esta consulta cambiaron” | Una capa de live query — sin cableado de eventos alguno |
| Proxies estrictos, infraestructura solo HTTP | Subscriptions sobre SSE |
| Equipo pequeño, sin apetito por ser dueño de un protocolo | Subscriptions tipadas en un backend gestionado |
Limitaciones y trade-offs
- Las subscriptions heredan la operación de los WebSockets. Apilar capas agrega significado, no magia: el estado de conexión, el enrutamiento con afinidad, los heartbeats y un backplane de pub/sub entre servidores siguen siendo la realidad de despliegue por debajo.
- La reconexión sigue perdiendo eventos. graphql-ws define streams, no reanudación: un socket caído significa frames perdidos, y ponerse al día (reconsultar y luego resuscribirse) es lógica de aplicación en cualquiera de las dos pilas.
- La resolución por suscriptor cuesta. Las subscriptions filtradas pueden ejecutar trabajo de resolvers y de permisos por evento y por suscriptor — un topic caliente con miles de oyentes lo multiplica; diseña los filtros del lado del servidor y estrechos.
- Circulan dos sub-protocolos. Los protocolos legado y moderno de GraphQL sobre WebSocket son mutuamente ininteligibles; los extremos desparejados fallan en silencio. Fija las versiones explícitamente.
- Los envelopes tipados cobran throughput. La serialización JSON y la validación de schema por evento son invisibles a decenas de eventos por segundo y dominantes a miles — mide antes de asumir en cualquier dirección.
GraphQL subscriptions y WebSockets en Back4app
Back4app es una plataforma open-source de Backend as a Service (BaaS) que combina base de datos gestionada, APIs REST y GraphQL generadas automáticamente, autenticación, almacenamiento de archivos y funciones serverless con Cloud Code. El apilamiento de este artículo se mapea directo sobre la plataforma: la API GraphQL generada automáticamente cubre las capas de query y mutation a partir de tu schema sin una línea de resolvers, mientras que la capa de tiempo real llega como Live Queries — eventos de subscription tipados y verificados por ACL sobre una flota gestionada de WebSockets, usando el protocolo abierto LiveQuery en lugar de graphql-ws y disparándose por cambios en el conjunto de resultados en vez de eventos cableados a mano. Te quedas con la columna del protocolo tipado de la tabla comparativa — multiplexación, autenticación y manejo de reconexión incluidos — sin operar la flota de sockets ni ser dueño de una especificación de protocolo.
Preguntas frecuentes
¿Las GraphQL subscriptions son lo mismo que los WebSockets?
No — viven en capas diferentes. Un WebSocket es transporte: una tubería de bytes persistente y full-duplex, sin opinión sobre lo que pasa por ella. Una GraphQL subscription es protocolo y contrato por encima: una operación definida en el schema cuyos eventos llegan tipados, validados y con la misma forma que cualquier otra respuesta GraphQL. Compararlas directamente es comparar una carretera con una línea de autobús.
¿Qué protocolo usan las GraphQL subscriptions?
Lo más común es graphql-ws, el sub-protocolo moderno de GraphQL sobre WebSocket: el cliente abre el socket, envía connection_init, recibe connection_ack y luego inicia operaciones con mensajes subscribe; el servidor transmite payloads next por id de subscription y cualquiera de los dos lados cierra con complete. Un sub-protocolo más antiguo de los inicios del ecosistema todavía circula, y por eso cliente y servidor deben acordar cuál de los dos hablan.
¿Pueden las GraphQL subscriptions correr sobre Server-Sent Events?
Sí — el contrato de subscription es agnóstico del transporte, y SSE es un portador legítimo cada vez más popular. Como el tráfico de subscriptions es abrumadoramente del servidor al cliente, un stream HTTP unidireccional encaja de forma natural, mantiene contentos a los proxies comunes y a la semántica HTTP, y trae reconexión automática gratis. Los WebSockets siguen siendo el default en la mayoría del tooling, pero "las subscriptions exigen WebSockets" es folclore, no un hecho.
¿Cuándo deberías usar WebSockets crudos en lugar de GraphQL subscriptions?
Cuando el tráfico deja de parecer eventos de API tipados: frames binarios (audio, estado de juego, streams de sensores), tasas de mensajes muy altas donde validar el schema y envolver en JSON cada evento cuesta throughput real, o protocolos que necesitan semántica propia — cursores, deltas, acknowledgments — que pelean con la forma de la subscription. Si aún no invertiste en un schema GraphQL, el socket crudo también evita importar uno solo por los eventos.
¿Las GraphQL subscriptions escalan?
El transporte escala como cualquier flota de WebSockets — estado de conexión, enrutamiento con afinidad y un backplane de pub/sub entre servidores. La capa de subscription agrega su propio eje: cada evento puede resolverse y filtrarse por suscriptor, así que un topic caliente con muchos suscriptores multiplica el trabajo de los resolvers. Las plataformas gestionadas absorben la flota; el diseño del schema y la disciplina de filtrado por suscriptor siguen siendo tuyos.
¿Las GraphQL subscriptions son lo mismo que las live queries?
Primas cercanas con disparadores distintos. Las subscriptions se disparan con eventos nombrados que cableas explícitamente — una mutation publica a un topic, los suscriptores reciben. Las live queries se disparan cuando cambia el conjunto de resultados de una consulta — sin cableado de eventos, con cada ruta de escritura cubierta automáticamente. Ambas suelen viajar sobre WebSockets. Una capa de live query cambia el diseño de eventos schema-first por detección automática de cambios sobre los datos mismos.
¿Por qué las subscriptions necesitan un sub-protocolo?
Porque un WebSocket pelado es solo bytes ordenados. En cuanto dos partes necesitan multiplexar varias subscriptions en un socket, correlacionar eventos con operaciones, negociar autenticación, señalizar errores y cerrar streams con limpieza, necesitan framing de mensajes y reglas — que cada equipo alguna vez inventó mal, una versión incompatible a la vez. graphql-ws estandariza exactamente esa capa para que clientes y servidores interoperen.