# 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
| Operation | Direction | Lifetime | Transport |
| Query | Client → Server (one request, one response) | Short | HTTP |
| Mutation | Client → Server (change + response) | Short | HTTP |
| Subscription | Server → 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