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

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

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

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

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

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

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

Часті запитання

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

  • image_website-b2b-advance_0.webp
    Розробка сайту компанії B2B ADVANCE
    1418
  • image_web-applications_feedme_466_0.webp
    Розробка веб-додатків для компанії FEEDME
    1286
  • image_websites_belfingroup_462_0.webp
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    983
  • image_ecommerce_furnoro_435_0.webp
    Розробка інтернет магазину для компанії FURNORO
    1243
  • image_crm_enviok_479_0.webp
    Розробка веб-додатків для компанії Enviok
    983
  • image_bitrix-bitrix-24-1c_fixper_448_0.webp
    Розробка веб-сайту для компанії ФІКСПЕР
    998

Реалізація 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 дні. Вартість проєкту розраховується індивідуально — економія на розробці за рахунок готових рішень.

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