Suscripciones de GraphQL para funcionalidades en tiempo real
Guía práctica para implementar suscripciones GraphQL en tiempo real: transporte WebSocket, resolvers, filtrado y consideraciones de escalado.

Las APIs REST siguen un patrón de solicitud-respuesta: el cliente pregunta, el servidor responde. Para funcionalidades en tiempo real como notificaciones en vivo, mensajes de chat o actualizaciones de dashboards, ese modelo se rompe. O haces polling repetidamente (desperdiciando ancho de banda y aumentando la latencia), o usas un mecanismo de push. Las suscripciones de GraphQL te dan semántica de push con la misma seguridad de tipos y el mismo diseño dirigido por el esquema que las queries y las mutations.
Las suscripciones usan conexiones WebSocket por debajo. El cliente declara qué eventos le interesan y el servidor envía datos a través de la conexión abierta cada vez que esos eventos ocurren.
Montar el servidor de suscripciones
La mayoría de servidores GraphQL soportan suscripciones mediante el protocolo graphql-ws. Aquí tienes una configuración con Apollo Server y 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);El detalle clave: HTTP atiende las queries y las mutations mientras que WebSocket atiende las suscripciones, ambos en el mismo endpoint /graphql. La librería graphql-ws gestiona el ciclo de vida del WebSocket — el handshake de conexión, el framing de los mensajes y los pings de keep-alive.
Definir los tipos de suscripción
Las suscripciones se definen en tu esquema junto a las queries y las mutations. Describen a qué eventos se pueden suscribir los clientes y qué forma tienen los datos que reciben.
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
}Implementar los resolvers de suscripción
Los resolvers de suscripción usan un patrón de AsyncIterator. Publicas eventos desde tu lógica de negocio y el resolver los filtra y los entrega a los suscriptores correctos.
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
});Gestionar las suscripciones en el cliente
En el cliente, las suscripciones se integran con tu cliente de GraphQL habitual. Aquí tienes un componente de React que usa 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;
}Escalar las suscripciones en producción
El PubSub en memoria de graphql-subscriptions sirve para una sola instancia del servidor. En producción, con varias instancias detrás de un balanceador de carga, necesitas un backend de pub/sub distribuido.
// ❌ 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
},
};Un pub/sub respaldado por Redis garantiza que un evento publicado por cualquier instancia del servidor llegue a todos los suscriptores, sin importar a qué instancia estén conectados. Este es el patrón estándar para servidores de suscripciones escalados horizontalmente.
Puntos clave
- Las suscripciones añaden semántica de push a GraphQL — los clientes declaran qué les interesa y los servidores envían los datos por conexiones WebSocket persistentes
- Usa
withFilterpara asegurarte de que cada suscriptor solo reciba los eventos relevantes según los argumentos de su query - Las claves del payload deben coincidir con los nombres de los campos de la suscripción — si no coinciden, la entrega es
nullen silencio - Sustituye el PubSub en memoria por Redis cuando ejecutes varias instancias del servidor detrás de un balanceador de carga
- Impón límites de conexión y autenticación en las conexiones WebSocket para evitar el agotamiento de recursos
- Las suscripciones complementan a las queries, no las sustituyen — la carga inicial usa una query y a partir de ahí las suscripciones transmiten las actualizaciones incrementales


