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:

npm 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.