Real-time GraphQL with Subscriptions in Node.js

Table of Contents

  1. What Are GraphQL Subscriptions?
  2. Real-time vs Traditional Data Fetching
  3. WebSockets and GraphQL
  4. Setting Up GraphQL Subscriptions in Node.js
  5. Using Apollo Server with Subscriptions
  6. Broadcasting Events with PubSub
  7. Example: Chat Application with Subscriptions
  8. Authentication in Subscriptions
  9. Scaling Subscriptions in Production
  10. Best Practices and Considerations

1. What Are GraphQL Subscriptions?

GraphQL Subscriptions enable real-time communication between a client and server. Unlike queries and mutations, which follow a request-response cycle, subscriptions use WebSockets to maintain a persistent connection, allowing the server to push updates to the client whenever a specific event occurs.


2. Real-time vs Traditional Data Fetching

Traditional GraphQL:

  • Client makes a request.
  • Server sends back data.
  • Connection ends.

GraphQL with Subscriptions:

  • Client subscribes to an event.
  • Server pushes new data whenever the event happens.
  • Persistent WebSocket connection remains open.

3. WebSockets and GraphQL

To implement GraphQL subscriptions, WebSockets are commonly used. WebSocket provides a full-duplex communication channel, which is perfect for pushing real-time updates from the server to connected clients.

Popular libraries for this include:

  • graphql-ws (modern, lightweight, recommended)
  • subscriptions-transport-ws (deprecated)

4. Setting Up GraphQL Subscriptions in Node.js

Prerequisites:

  • Node.js
  • Apollo Server
  • graphql-ws
  • graphql

Install Dependencies:

npm install apollo-server graphql graphql-ws ws

5. Using Apollo Server with Subscriptions

Apollo Server v3+ does not handle WebSockets directly. You need to integrate it with graphql-ws and an HTTP/WebSocket server manually.

Basic Setup:

const { createServer } = require('http');
const { WebSocketServer } = require('ws');
const { useServer } = require('graphql-ws/lib/use/ws');
const { ApolloServer } = require('apollo-server');
const { makeExecutableSchema } = require('@graphql-tools/schema');

const typeDefs = `
type Message {
content: String
}

type Query {
_empty: String
}

type Subscription {
messageSent: Message
}
`;

const { PubSub } = require('graphql-subscriptions');
const pubsub = new PubSub();

const resolvers = {
Subscription: {
messageSent: {
subscribe: () => pubsub.asyncIterator('MESSAGE_SENT')
}
}
};

const schema = makeExecutableSchema({ typeDefs, resolvers });

const server = new ApolloServer({ schema });
const httpServer = createServer();

(async () => {
await server.start();
server.applyMiddleware({ app: httpServer });

const wsServer = new WebSocketServer({
server: httpServer,
path: '/graphql',
});

useServer({ schema }, wsServer);

httpServer.listen(4000, () => {
console.log('Server running on http://localhost:4000/graphql');
});
})();

6. Broadcasting Events with PubSub

To broadcast data to all subscribed clients, use a pub-sub pattern.

Example Trigger:

setInterval(() => {
pubsub.publish('MESSAGE_SENT', {
messageSent: { content: "New message at " + new Date().toISOString() }
});
}, 5000);

Clients listening to messageSent will receive updates every 5 seconds.


7. Example: Chat Application with Subscriptions

Schema:

type Message {
id: ID!
content: String!
sender: String!
}

type Mutation {
sendMessage(content: String!, sender: String!): Message
}

type Subscription {
messageSent: Message
}

Resolver:

const resolvers = {
Mutation: {
sendMessage: (_, { content, sender }) => {
const message = { id: Date.now(), content, sender };
pubsub.publish('MESSAGE_SENT', { messageSent: message });
return message;
},
},
Subscription: {
messageSent: {
subscribe: () => pubsub.asyncIterator('MESSAGE_SENT'),
},
},
};

8. Authentication in Subscriptions

WebSockets don’t send HTTP headers after the initial handshake. To authenticate:

  • Send a token in the connection payload.
  • Validate the token before accepting the connection.

Example:

useServer({
schema,
onConnect: async (ctx) => {
const token = ctx.connectionParams?.authToken;
if (!validateToken(token)) throw new Error("Unauthorized");
}
}, wsServer);

9. Scaling Subscriptions in Production

WebSocket-based subscriptions can be tricky to scale across multiple instances.

Common strategies:

  • Redis PubSub: Share pub/sub events across servers.
  • Apollo Federation with Subscription Gateway
  • Use managed services like Hasura or GraphQL APIs with AWS AppSync.

Redis Example:

bashCopyEditnpm install graphql-redis-subscriptions ioredis
const { RedisPubSub } = require('graphql-redis-subscriptions');
const Redis = require('ioredis');

const pubsub = new RedisPubSub({
publisher: new Redis(),
subscriber: new Redis(),
});

10. Best Practices and Considerations

PracticeDescription
Use graphql-wsAvoid deprecated libraries
Always authenticateUse JWT or session tokens in connectionParams
Implement rate limitingPrevent abuse or spam
Use Redis for scaleScale subscriptions across clusters
Prefer Subscriptions for small payloadsDon’t overuse it for large datasets
Graceful fallbackProvide polling as fallback when WebSocket is unavailable

Conclusion

GraphQL Subscriptions unlock powerful real-time capabilities in your Node.js applications, from chat apps to collaborative tools. By combining WebSocket protocols, graphql-ws, and event broadcasting with robust authentication and scaling strategies, you can build reliable and responsive real-time systems.