GraphQL-Subscriptions für Echtzeit-Features
Praxisnahe Anleitung zu GraphQL-Subscriptions für Echtzeit-Updates: WebSocket-Transport, Subscription-Resolver, Filterung und Skalierung.

REST-APIs folgen einem Request-Response-Muster — der Client fragt, der Server antwortet. Für Echtzeit-Features wie Live-Benachrichtigungen, Chat-Nachrichten oder Dashboard-Updates bricht dieses Modell zusammen. Entweder pollst du permanent (was Bandbreite verschwendet und die Latenz erhöht) oder du setzt auf einen Push-Mechanismus. GraphQL-Subscriptions geben dir Push-Semantik mit derselben Typsicherheit und demselben schemagetriebenen Design wie Queries und Mutations.
Subscriptions nutzen unter der Haube WebSocket-Verbindungen. Der Client deklariert, welche Events ihn interessieren, und der Server pusht Daten über die offene Verbindung, sobald diese Events auftreten.
Den Subscription-Server aufsetzen
Die meisten GraphQL-Server unterstützen Subscriptions über das graphql-ws-Protokoll. Hier ein Setup mit Apollo Server und Express:
import { createServer } from 'http';
import express from 'express';
import { ApolloServer } from '@apollo/server';
import { expressMiddleware } from '@apollo/server/express4';
import { ApolloServerPluginDrainHttpServer } from '@apollo/server/plugin/drainHttpServer';
import { makeExecutableSchema } from '@graphql-tools/schema';
import { WebSocketServer } from 'ws';
import { useServer } from 'graphql-ws/lib/use/ws';
const app = express();
const httpServer = createServer(app);
// Create WebSocket server for subscriptions
const wsServer = new WebSocketServer({
server: httpServer,
path: '/graphql',
});
const schema = makeExecutableSchema({ typeDefs, resolvers });
// Set up graphql-ws handler
const serverCleanup = useServer({ schema }, wsServer);
const server = new ApolloServer({
schema,
plugins: [
ApolloServerPluginDrainHttpServer({ httpServer }),
{
async serverWillStart() {
return {
async drainServer() {
await serverCleanup.dispose();
},
};
},
},
],
});
await server.start();
app.use('/graphql', express.json(), expressMiddleware(server));
httpServer.listen(4000);Das entscheidende Detail: HTTP bedient Queries und Mutations, WebSocket bedient Subscriptions — beide über denselben /graphql-Endpunkt. Die Bibliothek graphql-ws verwaltet den Lebenszyklus des WebSockets: Verbindungs-Handshake, Message-Framing und Keep-alive-Pings.
Subscription-Typen definieren
Subscriptions werden in deinem Schema neben Queries und Mutations definiert. Sie beschreiben, welche Events Clients abonnieren können und welche Datenform sie dabei erhalten.
type Subscription {
# Simple subscription — receive every new message
messageCreated(channelId: ID!): Message!
# Filtered subscription — only specific order status changes
orderStatusChanged(orderId: ID!): OrderStatusUpdate!
# Broadcast subscription — all connected clients receive this
systemNotification: SystemNotification!
}
type Message {
id: ID!
channelId: ID!
author: User!
content: String!
createdAt: DateTime!
}
type OrderStatusUpdate {
orderId: ID!
previousStatus: OrderStatus!
newStatus: OrderStatus!
updatedAt: DateTime!
}
type SystemNotification {
id: ID!
level: NotificationLevel!
message: String!
timestamp: DateTime!
}
enum OrderStatus {
PENDING
PROCESSING
SHIPPED
DELIVERED
CANCELLED
}
enum NotificationLevel {
INFO
WARNING
CRITICAL
}Subscription-Resolver implementieren
Subscription-Resolver arbeiten nach dem AsyncIterator-Muster. Du veröffentlichst Events aus deiner Business-Logik, und der Resolver filtert sie und stellt sie den richtigen Subscribern zu.
import { PubSub, withFilter } from 'graphql-subscriptions';
const pubsub = new PubSub();
// Event name constants
const EVENTS = {
MESSAGE_CREATED: 'MESSAGE_CREATED',
ORDER_STATUS_CHANGED: 'ORDER_STATUS_CHANGED',
SYSTEM_NOTIFICATION: 'SYSTEM_NOTIFICATION',
} as const;
const resolvers = {
Subscription: {
messageCreated: {
// withFilter ensures clients only receive messages
// for the channel they subscribed to
subscribe: withFilter(
() => pubsub.asyncIterableIterator(EVENTS.MESSAGE_CREATED),
(payload, variables) => {
return payload.messageCreated.channelId === variables.channelId;
}
),
},
orderStatusChanged: {
subscribe: withFilter(
() => pubsub.asyncIterableIterator(EVENTS.ORDER_STATUS_CHANGED),
(payload, variables) => {
return payload.orderStatusChanged.orderId === variables.orderId;
}
),
},
systemNotification: {
// No filter — all subscribers receive all notifications
subscribe: () =>
pubsub.asyncIterableIterator(EVENTS.SYSTEM_NOTIFICATION),
},
},
Mutation: {
sendMessage: async (_: unknown, args: { channelId: string; content: string }, context: { userId: string }) => {
const message = await createMessage({
channelId: args.channelId,
authorId: context.userId,
content: args.content,
});
// Publish event — all matching subscribers receive it
await pubsub.publish(EVENTS.MESSAGE_CREATED, {
messageCreated: message,
});
return message;
},
},
};// ❌ Publishing without the correct payload shape
await pubsub.publish('MESSAGE_CREATED', { message: newMessage });
// Resolver expects payload.messageCreated, not payload.message
// Subscribers receive null/undefined — silent failure
// ✅ Payload key must match the subscription field name
await pubsub.publish('MESSAGE_CREATED', {
messageCreated: newMessage, // Matches subscription field name exactly
});Subscriptions auf der Client-Seite behandeln
Auf dem Client fügen sich Subscriptions in deinen bestehenden GraphQL-Client ein. Hier eine React-Komponente mit Apollo Client:
import { useSubscription, gql } from '@apollo/client';
const MESSAGE_SUBSCRIPTION = gql`
subscription OnMessageCreated($channelId: ID!) {
messageCreated(channelId: $channelId) {
id
content
author {
id
name
avatar
}
createdAt
}
}
`;
function ChatMessages({ channelId }: { channelId: string }) {
const { data, loading, error } = useSubscription(MESSAGE_SUBSCRIPTION, {
variables: { channelId },
onData: ({ data: subscriptionData }) => {
// Optional: handle each incoming message
const message = subscriptionData.data?.messageCreated;
if (message) {
playNotificationSound();
}
},
});
if (error) return <div>Connection error: {error.message}</div>;
if (loading) return <div>Connecting to channel...</div>;
const newMessage = data?.messageCreated;
return newMessage ? (
<div className="message">
<strong>{newMessage.author.name}:</strong> {newMessage.content}
</div>
) : null;
}Subscriptions in Produktion skalieren
Das In-Memory-PubSub aus graphql-subscriptions reicht für eine einzelne Serverinstanz. In Produktion, mit mehreren Instanzen hinter einem Load Balancer, brauchst du ein verteiltes Pub/Sub-Backend.
// ❌ In-memory PubSub — breaks with multiple server instances
import { PubSub } from 'graphql-subscriptions';
const pubsub = new PubSub();
// Server A publishes an event — only Server A's subscribers see it
// Clients connected to Server B miss the event entirely
// ✅ Redis-backed PubSub — works across all server instances
import { RedisPubSub } from 'graphql-redis-subscriptions';
import Redis from 'ioredis';
const pubsub = new RedisPubSub({
publisher: new Redis({ host: 'redis-host', port: 6379 }),
subscriber: new Redis({ host: 'redis-host', port: 6379 }),
});
// Server A publishes → Redis broadcasts → all servers deliver
// to their connected subscribers// Connection management for production
interface SubscriptionConfig {
maxConnectionsPerUser: number;
connectionTimeoutMs: number;
heartbeatIntervalMs: number;
maxSubscriptionsPerConnection: number;
}
const productionConfig: SubscriptionConfig = {
maxConnectionsPerUser: 5, // Prevent connection leaks
connectionTimeoutMs: 30_000, // Close idle connections
heartbeatIntervalMs: 10_000, // Detect dead connections
maxSubscriptionsPerConnection: 20, // Limit resource usage
};
// WebSocket server context with authentication
const wsServerOptions = {
schema,
context: async (ctx: { connectionParams: Record<string, unknown> }) => {
const token = ctx.connectionParams?.authorization as string;
if (!token) {
throw new Error('Missing authentication token');
}
const user = await verifyToken(token);
return { user };
},
onConnect: async (ctx: { connectionParams: Record<string, unknown> }) => {
console.log('Client connected');
// Validate authentication before allowing subscription
},
onDisconnect: () => {
console.log('Client disconnected');
// Clean up per-connection resources
},
};Ein Redis-gestütztes Pub/Sub stellt sicher, dass ein von irgendeiner Serverinstanz veröffentlichtes Event alle Subscriber erreicht — unabhängig davon, mit welcher Instanz sie verbunden sind. Das ist das Standardmuster für horizontal skalierte Subscription-Server.
Die wichtigsten Punkte
- Subscriptions ergänzen GraphQL um Push-Semantik — Clients deklarieren ihr Interesse, Server pushen Daten über persistente WebSocket-Verbindungen
- Nutze
withFilter, damit Subscriber nur die Events erhalten, die zu den Argumenten ihrer Query passen - Payload-Keys müssen den Feldnamen der Subscription entsprechen — bei Abweichungen wird stillschweigend
nullzugestellt - Ersetze In-Memory-PubSub durch Redis, sobald mehrere Serverinstanzen hinter einem Load Balancer laufen
- Erzwinge Verbindungslimits und Authentifizierung auf WebSocket-Verbindungen, um Ressourcenerschöpfung zu vermeiden
- Subscriptions ergänzen Queries, sie ersetzen sie nicht — der initiale Load läuft über eine Query, danach streamen Subscriptions die inkrementellen Updates


