---
term: 'GraphQL Subscriptions vs. WebSockets in Managed Backends'
seoTitle: 'GraphQL Subscriptions vs. WebSockets: Protocol vs. Transport'
headline: 'GraphQL Subscriptions vs. WebSockets: what''s actually being compared?'
slug: graphql-subscriptions-vs-websockets
category: api-realtime
shortDefinition: 'A GraphQL subscription is a typed, schema-defined event stream; a WebSocket is the raw transport it usually rides on.'
relatedTerms:
  - graphql
  - websockets-real-time-sync
  - real-time-live-queries
  - sse-vs-websockets-vs-polling
contrastsWith:
  - websockets-real-time-sync
aboutTerms:
  - 'GraphQL Subscriptions'
  - 'WebSockets'
faq:
  - question: 'Are GraphQL subscriptions the same as WebSockets?'
    answer: 'No — they sit in different layers. A WebSocket is a transport: a persistent, full-duplex byte pipe with no opinion about what crosses it. A GraphQL subscription is a protocol and contract layered on top: a schema-defined operation whose events arrive typed, validated, and shaped exactly like every other GraphQL response. Comparing them directly is comparing a road to a bus route.'
  - question: 'What protocol do GraphQL subscriptions use?'
    answer: 'Most commonly graphql-ws, the modern GraphQL-over-WebSocket sub-protocol: the client opens a socket, sends connection_init, receives connection_ack, then starts operations with subscribe messages; the server streams next payloads per subscription id and either side ends with complete. An older sub-protocol from the early ecosystem still circulates, which is why client and server must agree on which one they speak.'
  - question: 'Can GraphQL subscriptions run over Server-Sent Events instead?'
    answer: 'Yes — the subscription contract is transport-agnostic, and SSE is a legitimate carrier growing in popularity. Since subscription traffic is overwhelmingly server-to-client, a one-directional HTTP stream fits naturally, keeps ordinary proxies and HTTP semantics happy, and brings auto-reconnect for free. WebSockets remain the default in most tooling, but "subscriptions require WebSockets" is folklore, not fact.'
  - question: 'When should I use raw WebSockets instead of GraphQL subscriptions?'
    answer: 'When the traffic stops looking like typed API events: binary frames (audio, game state, sensor streams), very high message rates where per-event schema validation and JSON envelopes cost real throughput, or protocols needing custom semantics — cursors, deltas, acknowledgments — that fight the subscription shape. If you are not already invested in a GraphQL schema, a raw socket also avoids importing one just for events.'
  - question: 'Do GraphQL subscriptions scale?'
    answer: 'The transport scales like any WebSocket fleet — connection state, sticky routing, and a pub/sub backplane between servers. The subscription layer adds its own axis: each event may be resolved and filtered per subscriber, so a hot topic with many subscribers multiplies resolver work. Managed platforms absorb the fleet; schema design and per-subscriber filtering discipline stay yours.'
  - question: 'Are GraphQL subscriptions the same as live queries?'
    answer: 'Close cousins with different triggers. Subscriptions fire on named events you wire explicitly — a mutation publishes to a topic, subscribers receive. Live queries fire on result-set changes to a query — no event wiring, every write path covered automatically. Both usually ride WebSockets. A live query layer trades the schema-first event design for automatic change detection on the data itself.'
  - question: 'Why do subscriptions need a sub-protocol at all?'
    answer: 'Because a bare WebSocket is just ordered bytes. The moment two parties need to multiplex several subscriptions over one socket, correlate events to operations, negotiate authentication, signal errors, and end streams cleanly, they need message framing and rules — which every team once invented badly, one incompatible version each. graphql-ws standardizes exactly that layer so clients and servers can interoperate.'
codeLanguages: [javascript, dart, swift, kotlin]
externalAuthorities:
  - name: 'Subscriptions — graphql.org'
    url: 'https://graphql.org/learn/subscriptions/'
  - name: 'GraphQL over WebSocket Protocol (graphql-ws)'
    url: 'https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md'
  - name: 'RFC 6455 — The WebSocket Protocol'
    url: 'https://datatracker.ietf.org/doc/html/rfc6455'
  - name: 'The WebSocket API — MDN Web Docs'
    url: 'https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API'
  - name: 'GraphQL — Wikipedia'
    url: 'https://en.wikipedia.org/wiki/GraphQL'
  - name: 'WebSocket — Wikipedia'
    url: 'https://en.wikipedia.org/wiki/WebSocket'
cta:
  title: 'Real-time without owning either layer'
  text: 'Back4app pairs an auto-generated GraphQL API with Live Queries — typed, permission-checked subscriptions over a managed WebSocket fleet. Define the schema, subscribe to the query, and skip the protocol engineering entirely.'
  linkText: 'Start building free'
  linkUrl: 'https://www.back4app.com/signup'
author: 'Back4app Engineering'
publishedDate: '2026-08-05'
translationKey: graphql-subscriptions-vs-websockets
---

**A GraphQL subscription is a typed, schema-defined event stream; a WebSocket is the raw transport it usually rides on.** The "vs." in the title is a layering confusion worth untangling before any decision gets made: subscriptions are not an *alternative* to WebSockets — they are one of the things you can run *over* one, the way HTTP runs over TCP. The real choice is between a typed protocol someone else specified and a raw pipe whose protocol you invent.

## Key takeaways

| Question | Answer |
| --- | --- |
| WebSocket | A persistent, full-duplex byte pipe — no message semantics included |
| GraphQL subscription | A schema-defined event stream, typed and validated like any GraphQL response |
| Their relationship | Layered, not rival — subscriptions ride WebSockets (or SSE) via a sub-protocol |
| The sub-protocol | graphql-ws: init/ack handshake, ids multiplex operations, next/complete frames |
| The real decision | Typed protocol off the shelf vs. raw socket + a protocol you now own |

## The layering, in code

What a subscription actually is on the wire — a [graphql-ws](https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md) conversation inside a WebSocket:

```json
// client → server, after the socket opens
{ "type": "connection_init", "payload": { "authToken": "…" } }
// server → client
{ "type": "connection_ack" }
// client starts an operation — the id multiplexes this subscription
{ "type": "subscribe", "id": "1", "payload": {
    "query": "subscription { orderUpdated(status: PREPARING) { id status eta } }" } }
// server streams typed events, one frame per occurrence
{ "type": "next", "id": "1", "payload": { "data": { "orderUpdated": { "id": "o42", "status": "READY", "eta": null } } } }
// either side ends the stream
{ "type": "complete", "id": "1" }
```

And what most application code actually writes — a typed subscription with the whole stack managed:

**JavaScript:**

```javascript
// 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
// 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
```

**Swift:**

```swift
// 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
  }
}
```

**Kotlin:**

```kotlin
// 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) }
```

## One stack, three layers

```mermaid
flowchart TB
  accTitle: GraphQL subscriptions layered over a WebSocket transport
  accDescr: Application code consumes typed events. Below it, a subscription protocol such as graphql-ws handles handshake, multiplexing, and stream lifecycle. Below that, the transport layer is usually a WebSocket and sometimes Server-Sent Events. The transport moves bytes; the protocol gives them meaning; the schema gives them types.
  A["Application code<br/>typed events, schema-shaped payloads"] --> P["Subscription protocol — graphql-ws<br/>connection_init/ack · subscribe · next · complete"]
  P --> T["Transport — usually WebSocket, sometimes SSE<br/>persistent connection, ordered delivery"]
  T --> N["TCP/IP"]
```

The [WebSocket](/glossary/websockets-real-time-sync/) layer ([RFC 6455](https://datatracker.ietf.org/doc/html/rfc6455)) promises exactly this much: a persistent, full-duplex, ordered stream of text or binary frames, reachable from every browser via [one small API](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API). It says nothing about what a message *means* — no request/response correlation, no authentication convention, no error signaling, no way to run two logical streams over one socket. Every raw-socket project re-decides all of it.

The [subscription layer](https://graphql.org/learn/subscriptions/) is precisely that missing decision set, standardized: `connection_init`/`connection_ack` carries auth; per-operation `id`s multiplex many subscriptions over one socket; `next` frames deliver payloads that are ordinary GraphQL responses — typed by the schema, validated, introspectable, consumed by the same client machinery as queries and mutations; `complete` ends a stream without killing its neighbors. One historical footnote matters in practice: an older sub-protocol from the early ecosystem is still deployed, and a client speaking one to a server speaking the other fails in confusingly silent ways — pin the sub-protocol explicitly at both ends.

And because the contract is messages, not sockets, the transport underneath is swappable — the same subscription semantics increasingly run over Server-Sent Events, which suits the overwhelmingly one-directional shape of subscription traffic and inherits [SSE's proxy-friendliness and auto-reconnect](/glossary/sse-vs-websockets-vs-polling/). "Subscriptions vs. WebSockets" dissolves on contact: one is a contract, the other a carrier.

## GraphQL subscriptions vs. raw WebSockets

| | GraphQL subscriptions | Raw WebSockets |
| --- | --- | --- |
| Layer | Protocol + type system over a transport | The transport itself |
| Message contract | Schema-defined, validated, introspectable | Whatever you invent and document |
| Multiplexing | Built in — per-operation ids on one socket | Yours to design |
| Auth handshake | Standardized (`connection_init` payload) | Yours to design |
| Payloads | JSON, schema-shaped | Text **and binary**, any format |
| Filtering | Arguments on the subscription field | Server code you write |
| Overhead per event | JSON envelope + resolver execution | ~2–14 bytes of framing |
| Ecosystem | GraphQL clients, codegen, tooling | Bare socket libraries |
| Best when | Events are typed API data in a GraphQL app | Binary, high-frequency, or custom semantics |

## When raw sockets beat typed subscriptions

The honest cases are real, just narrower than raw-socket enthusiasm suggests. **Binary payloads** — audio chunks, protocol buffers, game state — ride WebSocket frames natively but would need encoding inside a JSON subscription envelope. **Message rate** — at thousands of events per second per client, the per-event resolver execution and JSON envelope stop being noise; a compact custom frame format is a legitimate optimization. **Custom semantics** — backpressure, client-side acks, delta encoding, resumable cursors — belong to protocols you design, and bolting them onto subscription frames fights the spec. And **no GraphQL to begin with** — adopting a schema, resolvers, and client tooling *just* to get typed events is the tail wagging the dog; a raw socket with a documented message format is smaller. The trap runs the other way too: teams that pick raw sockets for typed, JSON-shaped API events end up hand-writing multiplexing, auth handshakes, and reconnect semantics — a worse graphql-ws, one incompatible team at a time.

## Common use cases

- **Order and status streams** — "notify me when this order changes": typed API data, low rate, the subscription sweet spot.
- **Collaborative presence and comments** — subscriptions in GraphQL apps that already own the schema; the events are just more schema.
- **Financial tickers and dashboards** — subscriptions while payloads stay JSON-shaped; raw sockets when tick rate demands compact frames.
- **Chat** — either layer works; the deciding vote is usually whether the app is GraphQL-first, since [live queries](/glossary/real-time-live-queries/) cover the same ground without event wiring.
- **Multiplayer state and media** — binary, high-frequency, latency-critical: raw WebSocket territory, protocol and all.

## Should you use GraphQL subscriptions or raw WebSockets? A decision matrix

| Your situation | Reach for |
| --- | --- |
| App already speaks [GraphQL](/glossary/graphql/); events are typed API data | GraphQL subscriptions |
| Binary frames, or thousands of events/sec per client | Raw WebSockets |
| You need custom semantics — acks, deltas, cursors, backpressure | Raw WebSockets, protocol documented |
| No GraphQL investment, simple event feed | Raw socket with a small framed protocol — or SSE |
| Events are "this query's results changed" | A live query layer — no event wiring at all |
| Strict proxies, HTTP-only infrastructure | Subscriptions over SSE |
| Small team, no appetite for protocol ownership | Typed subscriptions on a managed backend |

## Limitations and trade-offs

- **Subscriptions inherit WebSocket operations.** Layering adds meaning, not magic: connection state, sticky routing, heartbeats, and a pub/sub backplane across servers remain the deployment reality underneath.
- **Reconnection still loses events.** graphql-ws defines streams, not resume: a dropped socket means missed frames, and catch-up (re-query, then re-subscribe) is application logic on either stack.
- **Per-subscriber resolution costs.** Filtered subscriptions can execute resolver and permission work per event per subscriber — a hot topic with thousands of listeners multiplies it; design filters server-side and narrow.
- **Two sub-protocols circulate.** Legacy and modern GraphQL-over-WebSocket protocols are mutually unintelligible; mismatched ends fail silently. Pin versions explicitly.
- **Typed envelopes tax throughput.** JSON serialization and schema validation per event are invisible at tens of events per second and dominant at thousands — measure before assuming either way.

## GraphQL subscriptions and WebSockets on Back4app

Back4app is an open-source Backend-as-a-Service (BaaS) platform that combines a managed database, auto-generated REST and GraphQL APIs, authentication, file storage, and Cloud Code serverless functions. The layering in this article maps directly onto the platform: the auto-generated GraphQL API covers the query and mutation layers from your schema with zero resolver code, while the real-time layer ships as [Live Queries](/glossary/real-time-live-queries/) — typed, ACL-checked subscription events over a managed WebSocket fleet, using the open LiveQuery protocol in place of graphql-ws and triggering on result-set changes rather than hand-wired events. You get the typed-protocol column of the comparison table — multiplexing, auth, reconnect handling included — without operating the socket fleet or owning a protocol spec.
