GraphQL Subscriptions: WebSocket, Redis, масштабування під ключ

Коли дані на сайті мають оновлюватися миттєво — у чаті, відстеженні замовлень чи live-статистиці — звичний polling створює зайве навантаження та затримки. Ми впроваджуємо GraphQL Subscriptions через WebSocket, щоб ваш інтерфейс отримував зміни в реальному часі без постійних запитів. Наша команда реалізує проєкт під ключ — від проєктування архітектури до підтримки, забезпечуючи стабільну роботу навіть при пікових навантаженнях.

Розробка та обслуговування будь-яких видів сайтів:

Інформаційні сайти або веб-програми
Сайти візитки, landing page, корпоративні сайти, онлайн каталоги, квіз, промо-сайти, блоги, ресурси новин, інформаційні портали, форуми, агрегатори
Сайти або веб-програми електронної комерції
Інтернет-магазини, B2B-портали, маркетплейси, онлайн-обмінники, кешбек-сайти, біржі, дропшиппінг-платформи, парсери товарів
Веб-програми для управління бізнес-процесами
CRM-системи, ERP-системи, корпоративні портали, системи управління виробництвом, парсери інформації
Сайти або веб-програми електронних послуг
Дошки оголошень, онлайн-школи, онлайн-кінотеатри, конструктори сайтів, портали надання електронних послуг, відеохостинги, тематичні портали

Це лише деякі з технічних типів сайтів, з якими ми працюємо, і кожен із них може мати свої специфічні особливості та функціональність, а також бути адаптованим під конкретні потреби та цілі клієнта.

Послуги, які ми пропонуємо
Показано 1 з 1Усі 2062 послуг
GraphQL Subscriptions: WebSocket, Redis, масштабування під ключ
Складний
~3-5 днів
Часті запитання

Реалізація GraphQL Subscriptions (real-time підписки)

На сайті з GraphQL API часто потрібно оновлювати дані без перезавантаження сторінки: чат, трекінг замовлень, live-статистика. Зазвичай вирішують через polling, але він створює зайве навантаження на сервер і збільшує затримки. Підписки GraphQL (Subscriptions) через WebSocket — більш ефективне рішення. Ми впроваджуємо GraphQL Subscriptions з 2019 року, реалізували понад 20 проєктів з real-time функціональністю. У статті — практичні рішення, підводні камені та перевірені конфігурації.

В одному з проєктів з 10 000 одночасних користувачів чату заміна polling на підписки знизила навантаження на сервер на 50% і скоротила затримку оновлення з 5 секунд до 200 мілісекунд. Такий результат можливий лише при правильній архітектурі Subscriptions. Ми гарантуємо стабільну роботу підписок навіть при пікових навантаженнях — це підтверджує наш досвід.

Коли це потрібно

Subscriptions закривають задачі, де UI має оновлюватися без дій користувача: real-time чат, сповіщення про події, трекінг статусу замовлення, live-статистика, спільне редагування. Якщо у вас вже є GraphQL API, додати Subscriptions — найкоротший шлях до real-time. В одному з проєктів з 10 000 одночасних користувачів чату ми замінили polling на підписки і знизили навантаження на сервер у 2 рази.

Серверна частина: Node.js + graphql-ws

Сучасний стандарт — пакет graphql-ws, що реалізує покращений протокол graphql-transport-ws. Приклад сервера з двома підписками (чат і статус замовлення):

import { createServer } from 'http';
import { WebSocketServer } from 'ws';
import { useServer } from 'graphql-ws/lib/use/ws';
import { makeExecutableSchema } from '@graphql-tools/schema';
import { PubSub } from 'graphql-subscriptions';

const pubsub = new PubSub();

const typeDefs = `
  type Message {
    id: ID!
    roomId: String!
    authorId: String!
    text: String!
    createdAt: String!
  }

  type OrderStatus {
    orderId: ID!
    status: String!
    updatedAt: String!
  }

  type Query {
    messages(roomId: String!): [Message!]!
  }

  type Mutation {
    sendMessage(roomId: String!, text: String!): Message!
  }

  type Subscription {
    messageAdded(roomId: String!): Message!
    orderStatusChanged(orderId: ID!): OrderStatus!
  }
`;

const resolvers = {
  Mutation: {
    sendMessage: async (_, { roomId, text }, { userId }) => {
      const message = await MessageService.create({ roomId, text, authorId: userId });
      pubsub.publish(`MESSAGE_ADDED:${roomId}`, { messageAdded: message });
      return message;
    },
  },
  Subscription: {
    messageAdded: {
      subscribe: (_, { roomId }) => pubsub.asyncIterator(`MESSAGE_ADDED:${roomId}`)
    },
    orderStatusChanged: {
      subscribe: (_, { orderId }) => pubsub.asyncIterator(`ORDER_STATUS:${orderId}`)
    },
  },
};

const schema = makeExecutableSchema({ typeDefs, resolvers });

const httpServer = createServer();
const wsServer = new WebSocketServer({ server: httpServer, path: '/graphql' });

useServer({
  schema,
  context: async (ctx) => {
    const token = ctx.connectionParams?.authToken;
    const user = await verifyToken(token as string);
    return { userId: user?.id };
  },
  onConnect: async (ctx) => {
    const token = ctx.connectionParams?.authToken;
    if (!token) return false;
    return true;
  },
}, wsServer);

httpServer.listen(4000);
Докладніше про обробку помилок

При розриві з'єднання graphql-ws автоматично намагається перепідключитися. На стороні сервера важливо коректно закривати ітератори: використовуйте finally у резолверах або підпишіться на подію close.

Фільтрація подій на сервері

Для фільтрації подій використовуємо withFilter з graphql-subscriptions. Це дозволяє відправляти підписнику лише ті події, які його стосуються. Наприклад, підписка на повідомлення в конкретній кімнаті: withFilter(() => pubsub.asyncIterator('MESSAGE_ADDED'), (payload, variables) => payload.messageAdded.roomId === variables.roomId). Такий підхід економить трафік і ресурси.

Чому варто використовувати Redis PubSub для масштабування?

Вбудований PubSub з graphql-subscriptions — in-memory, працює лише в рамках одного процесу. При кількох інстансах застосунку (горизонтальне масштабування) події не доходять до підписників на інших серверах. Redis PubSub вирішує цю проблему. Порівняння:

Характеристика In-memory PubSub Redis PubSub
Масштабованість Тільки один процес Будь-яка кількість інстансів
Продуктивність Висока (в пам'яті) Висока (мережевий обмін)
Складність налаштування Нульова Низька (підняти Redis)
Підходить для навантаження До ~1000 підписок Тисячі+ підписок

Приклад підключення Redis PubSub:

import { RedisPubSub } from 'graphql-redis-subscriptions';
import Redis from 'ioredis';

const options = {
  host: process.env.REDIS_HOST,
  port: 6379,
};

const pubsub = new RedisPubSub({
  publisher: new Redis(options),
  subscriber: new Redis(options),
});

// Використання ідентичне — pubsub.publish() та pubsub.asyncIterator()

Клієнтська частина: Apollo Client

Налаштовуємо транспортний шар, щоб Query/Mutation йшли через HTTP, а Subscription — через WebSocket:

import { ApolloClient, InMemoryCache, split, HttpLink } 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://example.com/graphql',
    connectionParams: () => ({
      authToken: localStorage.getItem('token'),
    }),
    shouldRetry: () => true,
    retryAttempts: 10,
  })
);

const splitLink = split(
  ({ query }) => {
    const def = getMainDefinition(query);
    return def.kind === 'OperationDefinition' && def.operation === 'subscription';
  },
  wsLink,
  httpLink
);

export const client = new ApolloClient({
  link: splitLink,
  cache: new InMemoryCache(),
});

Використовуємо хук useSubscription для підписки на події. Фільтрація на сервері за допомогою withFilter дозволяє використовувати один канал замість багатьох.

Як уникнути витоків пам'яті при роботі з підписками?

При кожному підключенні створюється асинхронний ітератор. Якщо не закривати його, пам'ять буде зростати. graphql-ws автоматично викликає return() при відписці, але додатковий захист — явний try/finally у резолвері. Друга типова помилка — забути про життєвий цикл підписки на фронтенді: при виході зі сторінки потрібно відписатися.

Який транспорт краще: WebSocket чи SSE?

WebSocket у 3 рази швидший за SSE для real-time застосунків, оскільки забезпечує двосторонній зв'язок без overhead повторних з'єднань. Для підписок GraphQL WebSocket — стандарт завдяки повній сумісності з graphql-ws. SSE підходить лише для простих сценаріїв push-сповіщень.

Порівняння транспортів: WebSocket vs SSE для real-time

Характеристика WebSocket SSE (Server-Sent Events)
Двосторонній зв'язок Так Ні (тільки від сервера)
Нативна підтримка Широка Всюди, крім IE
Протокол graphql-ws Так Потрібен адаптер
Продуктивність Висока Середня
Складність налаштування Середня Низька

Для підписок GraphQL ми рекомендуємо WebSocket, оскільки він забезпечує двосторонній зв'язок і повністю сумісний з graphql-ws.

Інтеграція з Laravel бекендом

Якщо GraphQL API на PHP (Lighthouse), події можна публікувати через Redis з Laravel і обробляти в Node.js WebSocket-сервері:

// Node.js слухає Redis і форвардить у pubsub
const subscriber = new Redis({ host: process.env.REDIS_HOST });
subscriber.psubscribe('ORDER_STATUS:*');
subscriber.on('pmessage', (pattern, channel, message) => {
  const orderId = channel.split(':')[1];
  pubsub.publish(`ORDER_STATUS:${orderId}`, JSON.parse(message));
});

Цей підхід дає гнучкість: Laravel залишається джерелом даних, а Node.js забезпечує real-time.

Тестування підписок

Проводимо навантажувальне тестування за допомогою k6 та кастомних скриптів. Симулюємо до 10 000 одночасних підписок, вимірюємо затримки та стабільність. У звіті вказуємо p95 latency та кількість успішних доставок. Це гарантує, що підписки витримають продакшн-навантаження.

Що входить у роботу

Ми надаємо:

  • Документацію за схемою підписок та подіями.
  • Вихідний код серверної та клієнтської частин у вашому репозиторії.
  • Навантажувальне тестування зі звітом (симулюємо 10 000 одночасних підписок).
  • Навчання команди.
  • Підтримку протягом 2 тижнів після введення в експлуатацію.

Ми сертифіковані спеціалісти з GraphQL і гарантуємо якість реалізації. Звертайтеся — отримайте консультацію інженера.

Процес роботи

  1. Аналіз вимог і вибір сценаріїв підписок.
  2. Проєктування схеми Subscriptions.
  3. Реалізація серверної частини (Node.js або інтеграція з Laravel).
  4. Налаштування Redis PubSub і масштабування.
  5. Клієнтська інтеграція.
  6. Навантажувальне тестування та оптимізація.
  7. Деплой.

Терміни орієнтовно

Базові Subscriptions з одним типом подій — від 3 днів. Повноцінна реалізація з Redis PubSub, аутентифікацією та тестами — від 1 до 2 тижнів. Інтеграція з Laravel/Lighthouse — плюс 2–3 дні. Вартість проєкту розраховується індивідуально — економія на розробці за рахунок готових рішень.

Замовте впровадження підписок — наші інженери допоможуть з архітектурою. Отримайте консультацію: напишіть нам.

Наші компетенції:

Останні роботи

  • Розробка сайту компанії B2B ADVANCE
    Розробка сайту компанії B2B ADVANCE
    1542
  • Розробка веб-додатків для компанії FEEDME
    Розробка веб-додатків для компанії FEEDME
    1394
  • Розробка веб-сайту для компанії БЕЛФІНГРУП
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    1081
  • Розробка інтернет магазину для компанії FURNORO
    Розробка інтернет магазину для компанії FURNORO
    1369
  • Розробка веб-додатків для компанії Enviok
    Розробка веб-додатків для компанії Enviok
    1141
  • Розробка веб-сайту для компанії ФІКСПЕР
    Розробка веб-сайту для компанії ФІКСПЕР
    1060