mrkeyoor.com_
Sun 20 Sept 17:53 UTC
npmWeb Backendupdated 20 Sept 2026

graphql-ws review

graphql-ws 6.2.1 implements the GraphQL over WebSocket protocol for clients and servers. Its client opens a lazy socket, multiplexes operations, handles protocol messages, retries connections, and exposes sink callbacks or an async iterator. Server adapters connect GraphQL execution to `ws`, Fastify, crossws, Bun, Deno, and other runtimes. It is transport code, not a pub/sub system or authentication product. Version 6.2 added a custom `parse` hook, and 6.2.1 fixes WebSocketServer typings. The wire format is intentionally incompatible with the retired `subscriptions-transport-ws` protocol.

Verdict

graphql-ws 6.2.1 is the focused choice when you have committed to GraphQL subscriptions over the current WebSocket protocol. Use SSE for one-way streams, and budget real work for auth expiry, reconnects, proxy timeouts, observability, and graceful shutdown.

We installed it

Lab card: what happened when we installed graphql-wsScreenshot of graphql-ws documentation
Install✓ · 2.5s2 packages on disk · 12 MB
ImportESM import works · require() works · ESM package with exports map
Browser46.2 KBgzipped (176.6 KB minified), bundled with esbuild
TypesTypeScript types bundled
Known vulns00 critical · 0 high · 0 moderate · 0 low (npm audit)

Answers from our run

Does graphql-ws install cleanly?

Yes. In a fresh container with an empty cache, npm install graphql-ws finished in 3 seconds, leaving 2 packages and 12 MB on disk. npm audit reported no known vulnerabilities.

How much does graphql-ws add to a browser bundle?

46.2 KB gzipped (176.6 KB minified) when the whole package is bundled for the browser with esbuild. Importing only part of it is usually smaller.

Does graphql-ws work with both ESM and CommonJS?

Yes. Both import 'graphql-ws' and require('graphql-ws') worked in Node 22 in our run. The package is published as ESM with an exports map.

Does graphql-ws include TypeScript types?

Yes, type declarations ship inside the package, so no @types install is needed.

graphql-ws or graphql-sse: which should you use?

graphql-sse: Use it for GraphQL streaming over HTTP when bidirectional WebSocket messages are unnecessary. graphql-ws 6.2.1 is the focused choice when you have committed to GraphQL subscriptions over the current WebSocket protocol.

When should you not use graphql-ws?

Updates only travel from server to client. graphql-sse uses HTTP and avoids WebSocket upgrade, idle-timeout, and connection-draining work.

API stability4/5The central `createClient`, `subscribe`, `iterate`, `useServer`, and lifecycle-hook shapes remain familiar across the current line. Version 6 made concrete migration changes: adapter imports lost `/lib/`, ws 7 and the deprecated Fastify package were dropped, and Fastify context renamed `connection` to `socket`. Releases 6.2 and 6.2.1 then added a parse hook and corrected server typings without changing the protocol contract.
Docs4/5The Guild site includes a start guide, adapter recipes, TypeDoc output, and direct links to the protocol document. The repository README clearly warns about incompatibility with `subscriptions-transport-ws`, which prevents a common handshake mistake. Operational details are thinner: teams still have to turn hooks and options into a policy for token expiry, connection limits, proxy timeouts, retry storms, draining, and pub/sub cleanup.
Maintenance5/5npm published 6.2.1 on 2026-08-06, and GitHub recorded a push that day. The repository is unarchived, has 1,871 stars, and reports 9 open issues and pull requests. Recent changes include GraphQL 17 support, a protocol-correct error termination fix, a custom parser hook, and repaired WebSocketServer types. The small open queue and release notes tied to pull requests make current work easy to inspect.
Ecosystem5/5npm counted 12,185,330 downloads for the week ending 2026-08-23. The project documents client and server integration across common GraphQL stacks and WebSocket implementations, while its four peer dependencies keep runtime choices outside the core package. That reach is useful only within the modern protocol: the older Apollo subscription transport remains a separate, incompatible ecosystem and cannot share a connection with this client.

Use it if

  • A GraphQL API needs subscriptions over WebSockets and both endpoints can speak the `graphql-transport-ws` subprotocol.
  • One client socket should carry several live operations with connection events, retry policy, ping/pong handling, and explicit disposal.
  • You already have a GraphQL schema and a supported WebSocket server, and want transport adapters instead of another application framework.
  • The same client API must work across browser, Node, Bun, Deno, Fastify, crossws, or uWebSockets deployments.
Skip it if

Setup reality

We installed graphql-ws 6.2.1 in a fresh unprivileged Node 22 Bookworm container. npm completed in 2.5 seconds, leaving 2 packages and 12 MB on disk. The package has 0 direct dependencies, 4 peer dependencies, and 368 KB unpacked. npm audit reported 0 known vulnerabilities at every severity. It includes TypeScript declarations and requires Node 20 or newer.

The package is ESM with an exports map; both require() and ESM import worked in our checks. Server use still needs the appropriate peer, such as GraphQL plus ws 8 or @fastify/websocket. Version 6 removed /lib/ from adapter subpaths, so import graphql-ws/use/ws. TypeScript should use an exports-aware module resolution mode. A browser build importing the whole package measured 176.6 KB minified and 46.2 KB gzipped, so import the client entry you need and keep server adapters out of frontend graphs.

Authentication normally starts in an async connectionParams function and is checked by onConnect or onSubscribe. Read the token on each connection so retries can pick up a refresh. The server must still enforce authorization for each operation. Connection authentication can become stale during a long subscription; graphql-ws does not renegotiate a token in place.

WebSocket operations outlive individual HTTP requests. Add heartbeat policy, connection limits, metrics, and a shutdown path that disposes the graphql-ws handler before closing the transport and HTTP server. Client retries need jitter to avoid a reconnect wave. Async-iterator consumers must exit or call return(), and subscription resolvers should release pub/sub listeners in finally when a socket disappears.

Patterns

Attach GraphQL to a ws server serve-with-ws

import { WebSocketServer } from 'ws';
import { useServer } from 'graphql-ws/use/ws';

const sockets = new WebSocketServer({ server: httpServer, path: '/graphql' });
const serverCleanup = useServer({ schema }, sockets);

Version 6 uses graphql-ws/use/ws without /lib/. Install ws and graphql as peers.

Open a browser subscription subscribe-from-browser

import { createClient } from 'graphql-ws';

const client = createClient({ url: 'wss://api.example.com/graphql' });
const stop = client.subscribe(
  { query: 'subscription { priceChanged { sku price } }' },
  { next: console.log, error: console.error, complete: () => {} },
);

The default client is lazy. Reuse one client per endpoint rather than opening a socket in every component.

Consume results with for await iterate-results

const stream = client.iterate({
  query: 'subscription($id: ID!) { commentAdded(postId: $id) { id body } }',
  variables: { id: postId },
});

try {
  for await (const result of stream) render(result.data?.commentAdded);
} finally {
  await stream.return?.();
}

The iterator can buffer while the consumer is busy. Always finish or return it so the server receives a completion message.

Read a fresh token on each connection authenticate-connection

const client = createClient({
  url,
  connectionParams: async () => ({ authorization: `Bearer ${await getToken()}` }),
});

useServer({
  schema,
  onConnect: async (ctx) => Boolean(await verify(ctx.connectionParams?.authorization)),
}, sockets);

A reconnect calls connectionParams again. An already-open socket does not refresh credentials by itself.

Reject an unauthorized subscription authorize-operation

useServer({
  schema,
  onSubscribe: async (ctx, id, payload) => {
    if (!canSubscribe(ctx.extra.user, payload)) {
      return [new GraphQLError('Forbidden')];
    }
  },
}, sockets);

Returning GraphQLError objects ends that operation without necessarily closing every operation on the socket.

Add bounded reconnect backoff configure-retries

const client = createClient({
  url,
  retryAttempts: 8,
  retryWait: async (attempt) => {
    const delay = Math.min(500 * 2 ** attempt, 15_000) + Math.random() * 500;
    await new Promise((resolve) => setTimeout(resolve, delay));
  },
});

Keep jitter. Identical delays make every browser reconnect together after a deployment or regional outage.

Monitor protocol ping and pong events send-heartbeats

const client = createClient({
  url,
  keepAlive: 12_000,
  on: {
    ping: (received) => metrics.count(received ? 'ping.in' : 'ping.out'),
    pong: (received) => metrics.count(received ? 'pong.in' : 'pong.out'),
  },
});

Protocol heartbeats do not override shorter idle limits in a proxy unless their interval stays below that limit.

Clean up an async generator resolver write-subscription-resolver

async function* subscribe(_root, { room }, ctx) {
  const events = ctx.pubsub.subscribe(`room:${room}`);
  try {
    for await (const message of events) yield { messageAdded: message };
  } finally {
    await events.return?.();
  }
}

Yield an object keyed by the GraphQL subscription field. finally releases the source after unsubscribe or disconnect.

Send only subscriptions through Apollo's WS link route-apollo-operations

const wsLink = new GraphQLWsLink(createClient({ url: wsUrl }));
const link = split(
  ({ query }) => {
    const operation = getMainDefinition(query);
    return operation.kind === 'OperationDefinition' && operation.operation === 'subscription';
  },
  wsLink,
  httpLink,
);

GraphQLWsLink targets this protocol. Older SubscriptionClient links target subscriptions-transport-ws and cannot handshake here.

Use the 6.2 parser hook customize-parser

useServer({
  schema,
  parse: (source) => {
    const document = persistedDocuments.get(String(source));
    if (!document) throw new GraphQLError('Unknown document');
    return document;
  },
}, sockets);

Do not put arbitrary query strings in an unbounded cache. A persisted-document allowlist gives the hook a finite input set.

Count active subscription operations observe-lifecycle

useServer({
  schema,
  onOperation: (_ctx, id) => metrics.gauge('subscriptions', 1, { id }),
  onComplete: (_ctx, id) => metrics.gauge('subscriptions', -1, { id }),
  onError: (_ctx, id, _payload, errors) => logger.warn({ id, errors }),
}, sockets);

Track connections and operations separately because one socket can carry many active subscriptions.

Drain sockets during shutdown shutdown-server

process.on('SIGTERM', async () => {
  await serverCleanup.dispose();
  await new Promise((resolve) => sockets.close(resolve));
  await new Promise((resolve) => httpServer.close(resolve));
});

Stopping HTTP upgrades alone leaves established sockets alive. Dispose the GraphQL handler before closing both servers.

Alternatives

PackageRegistryPick it when
graphql-ssenpmUse it for GraphQL streaming over HTTP when bidirectional WebSocket messages are unnecessary.
graphql-yoganpmUse it when you want a GraphQL server with transport integration and request handling assembled for you.
subscriptions-transport-wsnpmKeep it only as a legacy compatibility dependency for an endpoint that cannot move to the current protocol.

More web backend guides

urllib3 · requests · ws · anyio · httpx · undici · the whole shelf →

How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.