Redis Pub/Sub 어댑터로 실시간 기능 확장하기
수평 확장을 위해 Redis IoAdapter를 사용해 여러 인스턴스 간 소켓 상태를 동기화합니다
Redis Pub/Sub 어댑터로 실시간 기능 확장하기은(는) CoddyKit의 무료 NestJS Enterprise Backend APIs 강의입니다. 이것은 4개 중 4번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 NestJS Enterprise Backend APIs 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. NestJS Enterprise Backend APIs 강의에는 총 4개의 강의가 포함되어 있습니다.
이 강의의 일부는 아직 번역되지 않았으며 영어로 표시됩니다.
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 bycreateAdapter(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
RedisIoAdapterwith the app instance. awaitthe 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
retryStrategylets 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 attachcreateAdapter(...)increateIOServer. - Register it in
main.tswithapp.useWebSocketAdapter()beforelisten(); 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()anddisconnectSockets()round-trip through Redis. - Choose the Streams adapter instead when you need replay of broadcasts missed during a Redis blip.
자주 묻는 질문
“Redis Pub/Sub 어댑터로 실시간 기능 확장하기” 강의는 무료인가요?
네 — “Redis Pub/Sub 어댑터로 실시간 기능 확장하기” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 NestJS Enterprise Backend APIs 강의 전체를 잠금 해제할 수 있습니다. NestJS Enterprise Backend APIs 강의에는 총 4개의 강의가 포함되어 있습니다.
“Redis Pub/Sub 어댑터로 실시간 기능 확장하기”에서 뭘 배우나요?
수평 확장을 위해 Redis IoAdapter를 사용해 여러 인스턴스 간 소켓 상태를 동기화합니다 브라우저에서 직접 실행하는 실습 코드로 NestJS Enterprise Backend APIs을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.
NestJS Enterprise Backend APIs을(를) 시작하는 데 경험이 필요한가요?
사전 경험은 필요하지 않습니다. CoddyKit의 NestJS Enterprise Backend APIs은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 4번째 강의입니다.
“Redis Pub/Sub 어댑터로 실시간 기능 확장하기” 강의는 얼마나 걸리나요?
대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.
이 NestJS Enterprise Backend APIs 강의에서 코드를 작성하고 실행할 수 있나요?
네. 모든 NestJS Enterprise Backend APIs 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.
이 강의의 모든 강의
- Socket.IO 어댑터를 사용한 WebSocket 게이트웨이
- 소켓 연결 인증 및 보호
- 단방향 푸시를 위한 서버 전송 이벤트
- Redis Pub/Sub 어댑터로 실시간 기능 확장하기