NestJS Enterprise Backend APIs · Lekcja

Skalowanie komunikacji czasu rzeczywistego z adapterem Redis Pub/Sub

Synchronizuj stan socketów między wieloma instancjami za pomocą Redis IoAdapter, aby skalować horyzontalnie

Lekcja 4 z 413 kroki

Skalowanie komunikacji czasu rzeczywistego z adapterem Redis Pub/Sub to bezpłatna lekcja NestJS Enterprise Backend APIs na CoddyKit. To lekcja 4 z 4. Możesz przeczytać całą lekcję poniżej za darmo — a potem ćwiczyć ją interaktywnie w przeglądarce z wbudowanym edytorem kodu i tutorem AI dostępnym 24/7. To część ścieżki edukacyjnej NestJS Enterprise Backend APIs, a Twój postęp synchronizuje się między webem a aplikacją CoddyKit. Kurs NestJS Enterprise Backend APIs zawiera 4 lekcji w sumie.

Części tej lekcji nie zostały jeszcze przetłumaczone i są wyświetlane po angielsku.

The Multi-Instance Problem

A single NestJS WebSocket gateway keeps every connected socket in the memory of one Node process. The moment you scale horizontally behind a load balancer, that assumption breaks.

  • Client A connects to instance 1.
  • Client B connects to instance 2.
  • When instance 1 emits to a room, instance 2 never hears about it — so Client B misses the message.

To fix this we need a shared message bus that lets every instance broadcast events to every other instance. Redis Pub/Sub is the canonical choice, and Socket.IO ships an adapter for exactly this.

How the Redis Adapter Works

The @socket.io/redis-adapter replaces Socket.IO's in-memory adapter. Whenever your code calls server.to(room).emit(...), the adapter publishes that event to a Redis channel instead of only delivering it locally.

  • Every instance subscribes to the same Redis channels.
  • An emit on instance 1 is published to Redis, then every subscribed instance (including instance 2) receives it and delivers it to its own local sockets.
  • Redis is only a relay — socket connections still live on each node; no socket state is stored in Redis.

This means room membership, broadcasts, and even server.emit() fan out correctly across the whole cluster.

Installing the Pieces

You need three packages: a Redis client (ioredis or the official redis client), the Socket.IO Redis adapter, and Socket.IO itself (already pulled in by @nestjs/platform-socket.io).

The adapter needs two Redis connections: one pubClient for publishing and one subClient for subscribing. A connection in subscribe mode cannot issue normal commands, which is why they must be separate.

// package.json dependencies (excerpt)
{
  "dependencies": {
    "@nestjs/platform-socket.io": "^11.0.0",
    "@nestjs/websockets": "^11.0.0",
    "@socket.io/redis-adapter": "^8.3.0",
    "socket.io": "^4.7.0",
    "ioredis": "^5.4.0"
  }
}

A Custom IoAdapter

NestJS wraps Socket.IO behind an IoAdapter class. To inject the Redis adapter, you subclass IoAdapter and override createIOServer so that every namespace server is given the Redis adapter via server.adapter(...).

  • connectToRedis() creates the duplicated pub/sub clients once at bootstrap.
  • createIOServer() attaches the adapter created by createAdapter(pubClient, subClient).

This is the central piece of the whole lesson.

// redis-io.adapter.ts
import { IoAdapter } from '@nestjs/platform-socket.io';
import { ServerOptions } from 'socket.io';
import { createAdapter } from '@socket.io/redis-adapter';
import { Redis } from 'ioredis';

export class RedisIoAdapter extends IoAdapter {
  private adapterConstructor: ReturnType<typeof createAdapter>;

  async connectToRedis(url: string): Promise<void> {
    const pubClient = new Redis(url);
    const subClient = pubClient.duplicate();
    this.adapterConstructor = createAdapter(pubClient, subClient);
  }

  createIOServer(port: number, options?: ServerOptions): any {
    const server = super.createIOServer(port, options);
    server.adapter(this.adapterConstructor);
    return server;
  }
}

Wiring It at Bootstrap

The adapter must be connected to Redis before the app starts listening, and registered with app.useWebSocketAdapter(). Do this in main.ts.

  • Instantiate RedisIoAdapter with the app instance.
  • await the Redis connection so a failure aborts startup cleanly.
  • Register it, then call app.listen().
// main.ts
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { RedisIoAdapter } from './redis-io.adapter';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  const redisIoAdapter = new RedisIoAdapter(app);
  await redisIoAdapter.connectToRedis(
    process.env.REDIS_URL ?? 'redis://localhost:6379',
  );
  app.useWebSocketAdapter(redisIoAdapter);

  await app.listen(3000);
}
bootstrap();

The Gateway Stays Unchanged

The beauty of this approach: your @WebSocketGateway code does not change at all. You keep using server.to(room).emit() and the adapter transparently fans it out across instances.

  • Join a room with client.join(room) as usual.
  • Broadcast with this.server.to(room).emit(event, payload).

The same gateway now works whether you run 1 or 50 replicas.

// chat.gateway.ts
import {
  WebSocketGateway,
  WebSocketServer,
  SubscribeMessage,
  MessageBody,
  ConnectedSocket,
} from '@nestjs/websockets';
import { Server, Socket } from 'socket.io';

@WebSocketGateway({ cors: { origin: '*' } })
export class ChatGateway {
  @WebSocketServer() server: Server;

  @SubscribeMessage('joinRoom')
  onJoin(@ConnectedSocket() client: Socket, @MessageBody() room: string) {
    client.join(room);
    return { joined: room };
  }

  @SubscribeMessage('sendMessage')
  onMessage(@MessageBody() data: { room: string; text: string }) {
    // Fans out to every instance thanks to the Redis adapter
    this.server.to(data.room).emit('message', data.text);
  }
}

Sticky Sessions vs. Polling

The Redis adapter fixes broadcasting, but it does not fix the HTTP long-polling handshake. With multiple instances, Socket.IO's polling transport sends several HTTP requests during the upgrade; if those requests hit different instances, the handshake fails.

  • Option A: Enable sticky sessions on the load balancer so a client always reaches the same instance.
  • Option B: Force the WebSocket transport only, skipping polling entirely.

Most production setups enable sticky sessions at the ingress/load balancer layer.

// Force WebSocket-only to sidestep multi-request polling handshakes
@WebSocketGateway({
  transports: ['websocket'],
  cors: { origin: '*' },
})
export class ChatGateway {}

Graceful Reconnection of the Bus

If Redis goes down, the adapter cannot relay events between instances. With ioredis you get automatic reconnection out of the box, but you should log and observe failures so you know broadcasts are degraded.

  • Attach listeners to both pub and sub clients.
  • A retryStrategy lets you cap backoff and avoid hammering Redis.
// redis-io.adapter.ts (hardened connect)
async connectToRedis(url: string): Promise<void> {
  const pubClient = new Redis(url, {
    retryStrategy: (times) => Math.min(times * 100, 3000),
  });
  const subClient = pubClient.duplicate();

  for (const client of [pubClient, subClient]) {
    client.on('error', (err) => console.error('Redis adapter error', err));
    client.on('reconnecting', () => console.warn('Redis adapter reconnecting'));
  }

  this.adapterConstructor = createAdapter(pubClient, subClient);
}

Reasoning About Fan-Out Cost

Every cross-instance emit becomes Redis traffic. Understanding the fan-out helps you size Redis. A pure standalone calculation makes the scaling intuition concrete: if each broadcast must reach N instances, Redis relays roughly one publish that N−1 other instances consume.

The snippet below is a plain TypeScript program estimating published messages per second across a cluster — no framework or Redis needed to run it.

// Estimate Redis pub/sub relay load for a cluster
function relayLoad(
  instances: number,
  broadcastsPerSecPerInstance: number,
): { publishes: number; deliveries: number } {
  const publishes = instances * broadcastsPerSecPerInstance;
  // each publish is consumed by the other (instances - 1) nodes
  const deliveries = publishes * (instances - 1);
  return { publishes, deliveries };
}

const scenarios = [
  { instances: 2, rate: 100 },
  { instances: 10, rate: 100 },
  { instances: 50, rate: 100 },
];

for (const s of scenarios) {
  const { publishes, deliveries } = relayLoad(s.instances, s.rate);
  console.log(
    `${s.instances} instances -> ${publishes} publishes/s, ${deliveries} deliveries/s`,
  );
}

Targeting Specific Sockets Across Nodes

Beyond rooms, the Redis adapter also supports cluster-wide operations Socket.IO exposes on the server object:

  • server.fetchSockets() returns socket info from all instances.
  • server.in(socketId).disconnectSockets() can disconnect a socket living on another node.
  • server.socketsJoin(room) / socketsLeave(room) move sockets cluster-wide.

These are async because they round-trip through Redis to reach remote instances.

// admin.gateway.ts
@SubscribeMessage('kick')
async onKick(@MessageBody() socketId: string) {
  // Works even if that socket is connected to another instance
  const remoteSockets = await this.server.in(socketId).fetchSockets();
  for (const s of remoteSockets) {
    s.emit('kicked', { reason: 'admin action' });
    s.disconnect(true);
  }
}

Redis Pub/Sub vs. Streams Adapter

Socket.IO offers two Redis-based adapters with different delivery guarantees:

  • Pub/Sub adapter (@socket.io/redis-adapter): fire-and-forget. If an instance is briefly disconnected from Redis, it misses messages during that window. Lowest latency, simplest.
  • Streams adapter (@socket.io/redis-streams-adapter): uses Redis Streams so a reconnecting instance can replay missed messages. Slightly more overhead but better for at-least-once needs.

For typical chat/notification fan-out, the Pub/Sub adapter is the default recommendation. Choose Streams when missed broadcasts during a blip are unacceptable.

Quick Check: Why Two Clients?

The Redis adapter requires a separate pubClient and subClient. Why can't you reuse a single Redis connection for both?

Recap

You learned how to scale a NestJS realtime app horizontally with a Redis Pub/Sub adapter:

  • In-memory Socket.IO state does not span instances — Redis Pub/Sub relays broadcasts across the cluster.
  • Subclass IoAdapter, connect a pubClient and subClient, and attach createAdapter(...) in createIOServer.
  • Register it in main.ts with app.useWebSocketAdapter() before listen(); your gateway code stays unchanged.
  • Enable sticky sessions (or force the WebSocket transport) so the polling handshake survives load balancing.
  • Cluster-wide ops like fetchSockets() and disconnectSockets() round-trip through Redis.
  • Choose the Streams adapter instead when you need replay of broadcasts missed during a Redis blip.
Bezpłatny start

Ucz się TypeScript dzięki korepetycjom AI — za darmo

Pisz i uruchamiaj kod w przeglądarce, otrzymuj natychmiastową pomoc od korepetytora AI dostępnego 24/7 i kontynuuj naukę w sieci lub w aplikacji.

Kursy
20
Lekcje
76

Często zadawane pytania

Czy lekcja „Skalowanie komunikacji czasu rzeczywistego z adapterem Redis Pub/Sub” jest bezpłatna?

Tak — pełny tekst „Skalowanie komunikacji czasu rzeczywistego z adapterem Redis Pub/Sub” jest dostępny za darmo tutaj w sieci. Aby ćwiczyć ją interaktywnie (wbudowany edytor kodu i tutor AI dostępny 24/7) i odblokować resztę kursu NestJS Enterprise Backend APIs, przejdź na CoddyKit PRO. Kurs NestJS Enterprise Backend APIs zawiera 4 lekcji w sumie.

Co nauczysz się w „Skalowanie komunikacji czasu rzeczywistego z adapterem Redis Pub/Sub”?

Synchronizuj stan socketów między wieloma instancjami za pomocą Redis IoAdapter, aby skalować horyzontalnie Ćwiczysz NestJS Enterprise Backend APIs z praktycznym kodem, który uruchamiasz bezpośrednio w przeglądarce, a tutor AI dostępny 24/7 odpowiada na Twoje pytania podczas pracy nad lekcją.

Czy potrzebuję doświadczenia, aby zacząć NestJS Enterprise Backend APIs?

Nie wymagamy żadnego doświadczenia. NestJS Enterprise Backend APIs w CoddyKit jest strukturyzowany dla początkujących i zaawansowanych użytkowników, więc możesz zacząć tutaj lub od początku i uczyć się w swoim tempie. To lekcja 4 z 4.

Ile czasu zajmuje lekcja „Skalowanie komunikacji czasu rzeczywistego z adapterem Redis Pub/Sub”?

Większość lekcji CoddyKit trwa około 5–10 minut. Każda lekcja to mały, interaktywny krok, dzięki czemu robisz systematyczne postępy i zawsze wracasz dokładnie do tego samego miejsca — na webie i w aplikacji.

Czy mogę pisać i uruchamiać kod w tej lekcji NestJS Enterprise Backend APIs?

Tak. Każda lekcja NestJS Enterprise Backend APIs zawiera wbudowany edytor kodu, więc piszesz i uruchamiasz prawdziwy kod bezpośrednio w przeglądarce i od razu otrzymujesz sprzężenie zwrotne od AI — bez konfiguracji na komputerze.

Wszystkie lekcje w tym kursie

  1. Bramy WebSocket z adapterem Socket.IO
  2. Uwierzytelnianie i ochrona połączeń Socket
  3. Server-Sent Events do jednokierunkowego przesyłania
  4. Skalowanie komunikacji czasu rzeczywistego z adapterem Redis Pub/Sub
← Powrót do NestJS Enterprise Backend APIs