Redis Transport
The Redis transporter lets NestJS microservices communicate over Redis using its publish/subscribe mechanism. It is lightweight, trivial to operate (most teams already run Redis for caching), and a natural fit for fire-and-forget event broadcasting. Because Redis Pub/Sub does not persist messages or guarantee delivery, it shines for events where occasional loss is acceptable but is the wrong tool when you need durable, at-least-once queues.
How the Redis transporter works
NestJS implements request/response and event semantics on top of Redis channels. For every pattern, the framework derives two channels: one for the request (the pattern itself) and one for the reply (the pattern suffixed for the response). When a client emits or sends, it PUBLISHes to the request channel; the server SUBSCRIBEs to it, handles the message, and—for request/response—publishes the result back on the reply channel.
This is plain Redis Pub/Sub, which means messages are delivered only to subscribers connected at the moment of publish. There is no buffering, no acknowledgement, and no replay. A server that is restarting will miss every message published during the gap.
Warning: Redis Pub/Sub is at-most-once. If you require guaranteed delivery, retries, or durable queues, use the RabbitMQ or Kafka transporters instead. Note that NestJS uses Pub/Sub channels, not Redis Streams.
Installing dependencies
The Redis transporter is driven by the ioredis client.
npm install @nestjs/microservices ioredis
Configuring the server
Bootstrap a microservice with Transport.REDIS. The host and port point at your Redis instance; everything under options is passed through to the transporter.
// main.ts
import { NestFactory } from '@nestjs/core';
import { MicroserviceOptions, Transport } from '@nestjs/microservices';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.createMicroservice<MicroserviceOptions>(
AppModule,
{
transport: Transport.REDIS,
options: {
host: 'localhost',
port: 6379,
retryAttempts: 5,
retryDelay: 3000,
},
},
);
await app.listen();
}
bootstrap();
Message and event handlers are declared with the standard decorators. @MessagePattern participates in request/response; @EventPattern consumes one-way events.
// orders.controller.ts
import { Controller } from '@nestjs/common';
import { MessagePattern, EventPattern, Payload } from '@nestjs/microservices';
interface OrderCreatedEvent {
orderId: string;
total: number;
}
@Controller()
export class OrdersController {
@MessagePattern({ cmd: 'sum' })
accumulate(@Payload() data: number[]): number {
return (data || []).reduce((acc, n) => acc + n, 0);
}
@EventPattern('order.created')
handleOrderCreated(@Payload() event: OrderCreatedEvent): void {
console.log(`Fulfilling order ${event.orderId} ($${event.total})`);
}
}
Output:
Fulfilling order 1a2b-3c4d ($129.50)
Configuring the client
A consumer registers a ClientProxy with matching connection options, typically via ClientsModule.
// app.module.ts
import { Module } from '@nestjs/common';
import { ClientsModule, Transport } from '@nestjs/microservices';
import { GatewayController } from './gateway.controller';
@Module({
imports: [
ClientsModule.register([
{
name: 'ORDERS_SERVICE',
transport: Transport.REDIS,
options: { host: 'localhost', port: 6379 },
},
]),
],
controllers: [GatewayController],
})
export class AppModule {}
Inject the client and use send for request/response (returns an Observable) or emit for events.
// gateway.controller.ts
import { Controller, Get, Post, Body, Inject } from '@nestjs/common';
import { ClientProxy } from '@nestjs/microservices';
import { Observable } from 'rxjs';
@Controller('orders')
export class GatewayController {
constructor(@Inject('ORDERS_SERVICE') private readonly client: ClientProxy) {}
@Get('sum')
sum(): Observable<number> {
return this.client.send<number, number[]>({ cmd: 'sum' }, [1, 2, 3, 4]);
}
@Post()
create(@Body() body: { orderId: string; total: number }): void {
this.client.emit('order.created', body);
}
}
Tip:
emitdoes not wait for a handler and is the right choice for Redis events.sendopens a reply subscription and times out if no server responds—avoid it for pure broadcast scenarios.
Connection options
These options live under the options key and are forwarded to ioredis.
| Option | Type | Default | Purpose |
|---|---|---|---|
host | string | localhost | Redis server hostname. |
port | number | 6379 | Redis server port. |
retryAttempts | number | 0 | Number of reconnection attempts after a connection drop. |
retryDelay | number | 0 | Delay in milliseconds between reconnection attempts. |
wildcards | boolean | false | Use PSUBSCRIBE glob patterns instead of exact channels. |
password | string | — | Auth password (or use a connection URL). |
tls | object | — | TLS settings for secured Redis (e.g. managed cloud). |
You can also point at a Redis URL directly, which is convenient for managed providers:
options: {
host: process.env.REDIS_HOST,
port: Number(process.env.REDIS_PORT),
password: process.env.REDIS_PASSWORD,
tls: process.env.REDIS_TLS ? {} : undefined,
}
Redis pub/sub versus brokered queues
Choosing a transporter is mostly a question of delivery guarantees and operational weight.
| Concern | Redis Pub/Sub | RabbitMQ / Kafka |
|---|---|---|
| Delivery guarantee | At-most-once | At-least-once (durable) |
| Message persistence | None | Yes |
| Offline consumer | Misses messages | Receives on reconnect |
| Operational cost | Very low | Higher |
| Latency | Extremely low | Low–moderate |
| Best for | Cache invalidation, presence, transient events | Orders, payments, work queues |
Reach for Redis when the event is informational and replaceable: cache busting, live dashboards, “user is typing” signals. Reach for a broker when missing a message is a business problem.
Best practices
- Prefer
@EventPattern/emitover@MessagePattern/send—Redis Pub/Sub is built for broadcast, not durable request/response. - Treat events as disposable: never put data through Redis Pub/Sub that you cannot afford to lose.
- Set
retryAttemptsandretryDelayso transient network blips do not permanently detach a service. - Reuse your existing caching Redis only for low-stakes signals; isolate critical messaging onto a broker.
- Secure connections in production with
passwordandtls, and load credentials from environment variables. - Namespace your patterns (e.g.
order.created,user.updated) to keep channels readable and collision-free. - Make event handlers idempotent—even at-most-once delivery can produce duplicates under reconnection edge cases.