Client Area
GraphQL APIsAdvanced

GraphQL Subscriptions with WebSockets on DomainIndia VPS

ByDomain India Team·DomainIndia Engineering
6 min read24 Apr 20263 views
# GraphQL Subscriptions with WebSockets on DomainIndia VPS
TL;DR
GraphQL subscriptions deliver real-time updates over WebSockets — for chat, live dashboards, notifications, collaborative editing. This guide covers Apollo Server subscriptions, graphql-ws protocol, Redis pub/sub for multi-instance scaling, and the nginx WebSocket proxy setup on DomainIndia VPS.
## Queries vs Mutations vs Subscriptions
OperationDirectionLifetimeTransport
QueryClient → Server (one request, one response)ShortHTTP
MutationClient → Server (change + response)ShortHTTP
SubscriptionServer → Client (many events over time)Long (until client disconnects)WebSocket
Subscriptions let the server push events: new message arrived, stock price changed, build finished. ## Architecture on DomainIndia VPS ``` Client (browser/app) ───WebSocket──► nginx ───proxy──► Apollo Server ◄──pub/sub──► Redis │ Mutations/Queries via same WS connection ``` Key pieces: 1. **Apollo Server** with `graphql-ws` subscription transport 2. **Redis** for pub/sub across multiple Apollo instances 3. **nginx** with `Upgrade` + `Connection` headers for WebSocket proxy ## Step 1 — Install dependencies ```bash npm install @apollo/server graphql graphql-ws ws graphql-subscriptions graphql-redis-subscriptions ioredis express @as-integrations/express4 ``` ## Step 2 — Define subscription schema ```javascript const typeDefs = `#graphql type Message { id: ID! text: String! author: String! createdAt: String! } type Query { messages: [Message!]! } type Mutation { postMessage(text: String!, author: String!): Message! } type Subscription { messageAdded: Message! } `; ``` ## Step 3 — Implement resolvers with PubSub ```javascript import { RedisPubSub } from 'graphql-redis-subscriptions'; import Redis from 'ioredis'; const options = { host: process.env.REDIS_HOST || 'localhost', port: 6379, }; const pubsub = new RedisPubSub({ publisher: new Redis(options), subscriber: new Redis(options), }); const MESSAGE_ADDED = 'MESSAGE_ADDED'; const resolvers = { Query: { messages: () => db.message.findMany(), }, Mutation: { postMessage: async (_, { text, author }) => { const msg = await db.message.create({ data: { text, author } }); pubsub.publish(MESSAGE_ADDED, { messageAdded: msg }); return msg; }, }, Subscription: { messageAdded: { subscribe: () => pubsub.asyncIterator([MESSAGE_ADDED]), }, }, }; ```
Info

Why Redis pub/sub (not in-memory)? If you run 2+ Apollo instances behind a load balancer, a mutation on instance A must reach subscribers on instance B. Redis routes messages between them. In-memory works only with 1 instance — fragile.

## Step 4 — Wire up server with WS support ```javascript import express from 'express'; import http from 'http'; import { WebSocketServer } from 'ws'; import { useServer } from 'graphql-ws/lib/use/ws'; import { makeExecutableSchema } from '@graphql-tools/schema'; import { ApolloServer } from '@apollo/server'; import { expressMiddleware } from '@as-integrations/express4'; import { ApolloServerPluginDrainHttpServer } from '@apollo/server/plugin/drainHttpServer'; const schema = makeExecutableSchema({ typeDefs, resolvers }); const app = express(); const httpServer = http.createServer(app); const wsServer = new WebSocketServer({ server: httpServer, path: '/graphql', }); const serverCleanup = useServer({ schema, context: async (ctx) => { // Auth via connection_init params const token = ctx.connectionParams?.authorization?.replace('Bearer ', ''); const user = token ? await verifyToken(token) : null; return { user, pubsub }; }, onConnect: async (ctx) => { // Reject unauthenticated connections if (!ctx.connectionParams?.authorization) { throw new Error('Missing auth token'); } }, }, wsServer); const apollo = new ApolloServer({ schema, plugins: [ ApolloServerPluginDrainHttpServer({ httpServer }), { async serverWillStart() { return { async drainServer() { await serverCleanup.dispose(); } }; }, }, ], }); await apollo.start(); app.use('/graphql', express.json(), expressMiddleware(apollo, { context: async ({ req }) => ({ user: req.user, pubsub }), })); httpServer.listen(4000, () => { console.log('http://localhost:4000/graphql (also WS)'); }); ``` ## Step 5 — nginx WebSocket reverse proxy `/etc/nginx/conf.d/gql.conf`: ```nginx map $http_upgrade $connection_upgrade { default upgrade; '' close; } upstream graphql { server 127.0.0.1:4000; } server { listen 80; server_name api.yourcompany.com; location /graphql { proxy_pass http://graphql; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-Proto $scheme; # WS connections stay open — no short timeout proxy_read_timeout 3600s; proxy_send_timeout 3600s; } } ``` The `map` block + `Upgrade`/`Connection` headers are what make WS work through nginx. ## Step 6 — Client (browser) ```javascript import { ApolloClient, InMemoryCache, HttpLink, split } from '@apollo/client'; import { GraphQLWsLink } from '@apollo/client/link/subscriptions'; import { createClient } from 'graphql-ws'; import { getMainDefinition } from '@apollo/client/utilities'; const httpLink = new HttpLink({ uri: '/graphql' }); const wsLink = new GraphQLWsLink(createClient({ url: 'wss://api.yourcompany.com/graphql', connectionParams: () => ({ authorization: `Bearer ${localStorage.getItem('token')}`, }), retryAttempts: Infinity, shouldRetry: () => true, })); // Split: subscriptions go to WS, queries/mutations to HTTP const splitLink = split( ({ query }) => { const def = getMainDefinition(query); return def.kind === 'OperationDefinition' && def.operation === 'subscription'; }, wsLink, httpLink, ); const client = new ApolloClient({ link: splitLink, cache: new InMemoryCache(), }); // Usage: client.subscribe({ query: gql`subscription { messageAdded { id text author } }`, }).subscribe({ next: ({ data }) => console.log('New message:', data.messageAdded), }); ``` ## Authentication and filtering Subscriptions must filter — not every subscriber gets every event. ```javascript Subscription: { orderStatusChanged: { subscribe: withFilter( () => pubsub.asyncIterator('ORDER_STATUS'), (payload, variables, context) => { // Only deliver to the order's owner return payload.orderStatusChanged.userId === context.user.id && payload.orderStatusChanged.orderId === variables.orderId; } ), }, } ``` Without filtering, every subscriber sees every event — huge privacy leak. ## Scaling — when one VPS isn't enough - 100 concurrent WS connections: 1 VPS (2 GB RAM), no problem - 1,000: 1 VPS (4 GB RAM), monitor memory - 10,000: 2-3 VPS behind a load balancer (sticky sessions not required with Redis pub/sub) - 100,000+: dedicated infra, consider PushPin, Socket.IO Redis adapter, or managed service like Ably Each WS connection uses ~50–100 KB of Node.js heap. Budget accordingly. ## Heartbeats and reconnection WebSocket connections drop — mobile networks, NAT timeouts, load balancer restarts. graphql-ws sends `ping`/`pong` every 30s by default. Configure: ```javascript useServer({ schema, keepAlive: 12000, // 12s — good for mobile }, wsServer); ``` Client reconnection is automatic with `retryAttempts: Infinity`. ## Common pitfalls ## FAQ
Q Server-Sent Events (SSE) vs WebSocket for subscriptions?

SSE is one-way (server→client), simpler, works over HTTP/2. graphql-sse exists and is fine for pure subscriptions. WS is two-way; needed if same connection serves queries too.

Q Can I run this on DomainIndia shared hosting?

Passenger has limited WS support. Node.js apps in cPanel's "Setup Node.js App" generally don't proxy WS cleanly. For production subscriptions, use VPS.

Q Hasura subscriptions vs custom Apollo?

Hasura gives you subscriptions for free on every PostgreSQL view/table. Great for "notify on row change". Apollo for custom event logic (e.g. subscription filtered by auth, business rules).

Q Do subscriptions replace Redis pub/sub for my app?

Only for subscribed clients. Internal service-to-service messaging is still better via direct Redis pub/sub or a queue. Subscriptions are the end-of-chain delivery to end users.

Q How do I debug WebSocket issues?

Browser devtools → Network tab → WS filter. wscat CLI tool for raw WS testing. tcpdump on VPS for low-level inspection. nginx error log for proxy issues.

Production WebSocket/GraphQL subscriptions need a VPS. See VPS plans

Was this article helpful?

Your feedback helps us improve our documentation

Still need help? Submit a support ticket