57 Introduction to WebSocket for Real-time GraphQL
57 Introduction to WebSocket for Real-time GraphQL
The web has evolved far beyond static HTML documents being shipped from server to browser. Today, users expect real-time interaction, instant data synchronization, and extreme responsiveness. In the world of modern applications—whether it is a chat feature, an auto-refreshing dashboard, or a continuously updating feed—everything boils down to one thing: real-time communication between client and server.
GraphQL has become the de facto standard for APIs at many companies thanks to its flexibility and efficiency. But what happens when our application needs data updates that are truly real-time? This is exactly where WebSocket comes in and integrates seamlessly with GraphQL through a feature known as subscriptions.
This article will cover the fundamental concepts of WebSocket, how it works with GraphQL, how to implement subscriptions, and the best practices around them. We will walk through code, simulations, comparison tables, and flow diagrams. This is not just a tutorial—it is a roadmap for anyone who wants to bring real-time capabilities to their API.
What Is WebSocket?
WebSocket is a network communication protocol that provides a two-way (full-duplex) channel between client and server over a single TCP connection. WebSocket differs from traditional HTTP (request-response) because it allows the server to push messages to the client at any time, without having to wait for a request.
When Is WebSocket Better Than HTTP?
| HTTP (Polling/Long Polling) | WebSocket |
|---|---|
| The client must initiate every request | The server can push data to the client spontaneously |
| High overhead—every request behaves like a fresh handshake | Just a single handshake, with lightweight communication afterward |
| Higher latency for real-time data | Latency as low as possible |
| Suited for static content or traditional CRUD | Suited for chat, notifications, games, live dashboards, etc. |
GraphQL and WebSocket: Why Subscriptions?
GraphQL has three core operations:
- Query: Fetch data.
- Mutation: Change data.
- Subscription: Listen for data changes.
Subscriptions are the feature that lets a client receive real-time data the moment something changes on the server, delivered over a WebSocket channel.
Data Flow: Query vs Subscription
Query:
- The client sends a GraphQL query (HTTP).
- The server returns a single response.
- The connection is closed.
Subscription:
- The client opens a WebSocket connection.
- The client subscribes to a specific event.
- Whenever an event or change occurs, the server automatically pushes data to the client.
- The connection stays alive for as long as the client needs it.
Let’s look at a simple WebSocket flow diagram using mermaid:
sequenceDiagram
participant C as Client
participant S as Server
C->>S: Membuka koneksi WebSocket
C->>S: Mengirim GraphQL Subscription
S->>C: Kirim ACK (Berhasil subscribe)
loop Setiap ada perubahan data
S->>C: Kirim data terbaru (event)
end
C-->>S: Tutup koneksi WebSocket
Basic Implementation: GraphQL Subscription with WebSocket
We will use a popular stack: Node.js (Express), Apollo Server, and graphql-ws.
Server Setup
Install the dependencies:1npm install express apollo-server-express graphql graphql-wsGraphQL Schema (subscription, query, mutation)
1// schema.js 2const { gql } = require('apollo-server-express'); 3 4module.exports = gql` 5 type Message { 6 id: ID! 7 content: String! 8 user: String! 9 } 10 11 type Query { 12 messages: [Message!] 13 } 14 15 type Mutation { 16 postMessage(content: String!, user: String!): ID! 17 } 18 19 type Subscription { 20 messagePosted: Message! 21 } 22`;Resolvers with PubSub
1// resolvers.js 2const { PubSub } = require('graphql-subscriptions'); 3const pubsub = new PubSub(); 4const messages = []; 5 6module.exports = { 7 Query: { 8 messages: () => messages 9 }, 10 Mutation: { 11 postMessage: (_, { content, user }) => { 12 const id = messages.length + 1; 13 const message = { id, content, user }; 14 messages.push(message); 15 pubsub.publish('MESSAGE_POSTED', { messagePosted: message }); 16 return id; 17 } 18 }, 19 Subscription: { 20 messagePosted: { 21 subscribe: () => pubsub.asyncIterator(['MESSAGE_POSTED']) 22 } 23 } 24};Apollo Server + graphql-ws
1// index.js 2const express = require('express'); 3const { ApolloServer } = require('apollo-server-express'); 4const { createServer } = require('http'); 5const { useServer } = require('graphql-ws/lib/use/ws'); 6const { WebSocketServer } = require('ws'); 7const typeDefs = require('./schema'); 8const resolvers = require('./resolvers'); 9 10async function start() { 11 const app = express(); 12 const httpServer = createServer(app); 13 const apolloServer = new ApolloServer({ typeDefs, resolvers }); 14 15 await apolloServer.start(); 16 apolloServer.applyMiddleware({ app }); 17 18 // WebSocket server 19 const wsServer = new WebSocketServer({ 20 server: httpServer, 21 path: '/graphql', 22 }); 23 24 useServer({ schema: apolloServer.schema }, wsServer); 25 26 httpServer.listen(4000, () => { 27 console.log(`🚀 Server siap di http://localhost:4000${apolloServer.graphqlPath}`); 28 console.log(`⚡ WS siap di ws://localhost:4000/graphql`); 29 }); 30} 31 32start();Client Subscriptions
If you are using Apollo Client in React:1import { createClient } from 'graphql-ws'; 2import { ApolloClient, InMemoryCache, split, HttpLink } from '@apollo/client'; 3import { GraphQLWsLink } from '@apollo/client/link/subscriptions'; 4import { getMainDefinition } from '@apollo/client/utilities'; 5 6const wsLink = new GraphQLWsLink(createClient({ 7 url: 'ws://localhost:4000/graphql', 8})); 9 10const httpLink = new HttpLink({ 11 uri: 'http://localhost:4000/graphql', 12}); 13 14// Split based on operation type 15const splitLink = split( 16 ({ query }) => { 17 const definition = getMainDefinition(query); 18 return ( 19 definition.kind === 'OperationDefinition' && 20 definition.operation === 'subscription' 21 ); 22 }, 23 wsLink, 24 httpLink 25); 26 27const client = new ApolloClient({ 28 link: splitLink, 29 cache: new InMemoryCache(), 30});An example of subscribing in React:
1import { gql, useSubscription } from '@apollo/client'; 2 3const MESSAGE_POSTED = gql` 4 subscription { 5 messagePosted { 6 id 7 content 8 user 9 } 10 } 11`; 12 13function Chat() { 14 const { data } = useSubscription(MESSAGE_POSTED); 15 // render chat messages using data 16}
Simulation: Real-time Chat Workflow
- User A sends a message via the
postMessagemutation. - The server publishes the event to all subscribers.
- Every client with the
messagePostedsubscription automatically receives the new message without reloading.
Comparison Table: Subscription vs Polling
| Feature | Polling with setInterval | GraphQL Subscription (WebSocket) |
|---|---|---|
| Latency | Delayed until the next interval | Almost instant, real-time |
| Server Resources | Many connections & high CPU | A single connection & scalable |
| Data Delivery | Inefficient, refetches everything | Only the latest event data |
| Scale | Poor | Designed to fan out to thousands of clients |
| Real-time | Pseudo | Genuinely real-time |
Best Practices and Challenges
- Scalability: Use a pub-sub broker (such as Redis) for multi-instance servers.
- Security: Always perform authentication during the WebSocket handshake.
- Backpressure & Disconnect: Prepare logic to handle poor networks or client disconnects.
- Protocol: Use
graphql-ws(not subscriptions-transport-ws, which is deprecated). - Monitoring: Track active connections and WebSocket throughput.
Conclusion
WebSocket + GraphQL Subscriptions are the key to delivering real-time applications that are both scalable and elegant. The implementation is indeed technical, but with a solid foundation we can build collaboration features, notifications, and instant reporting with just a few extra lines of code on top of an ordinary GraphQL API.
Remember, real-time is no longer a luxury—it is an absolute necessity for today’s applications.
Want to dig deeper? Try adding Redis PubSub for scaling, or use a package like Hasura for building real-time GraphQL out of the box. Happy building!
References: