Skip to main content

How do Microservices work in NestJS?

NestJS microservices are independent services that communicate through message transports like TCP, Redis, or gRPC, using the same decorators and modules as regular HTTP apps but without Express or Koa underneath.

Theory

TL;DR

  • Think of it like a kitchen brigade: the API gateway (head waiter) takes HTTP requests and dispatches them as messages to specialized stations (math-service, user-service), each listening on its own transport.
  • Main difference: HTTP controllers use @Get/@Post; microservice controllers use @MessagePattern and listen on TCP, Redis, or Kafka instead of HTTP.
  • Two communication styles: request-response (@MessagePattern + client.send()) and fire-and-forget (@EventPattern + client.emit()).
  • Decision rule: two or more services need to scale independently or talk without HTTP? Use microservices. Single team, single deploy? Stay with HTTP controllers.
  • @nestjs/microservices ships seven built-in transports: TCP, Redis, NATS, RabbitMQ, Kafka, gRPC, and MQTT.

Quick example

A TCP math service: server listens on port 3001, client sends two numbers and gets the sum back.

typescript
// math-service/main.ts import { NestFactory } from '@nestjs/core'; import { MicroserviceOptions, Transport } from '@nestjs/microservices'; import { MathModule } from './math.module'; async function bootstrap() { const app = await NestFactory.createMicroservice<MicroserviceOptions>(MathModule, { transport: Transport.TCP, options: { host: 'localhost', port: 3001 }, }); await app.listen(); // no HTTP port, just TCP } bootstrap();
typescript
// math.controller.ts import { Controller } from '@nestjs/common'; import { MessagePattern, Payload } from '@nestjs/microservices'; @Controller() export class MathController { @MessagePattern({ cmd: 'add' }) // matches client.send({ cmd: 'add' }, ...) add(@Payload() data: number[]): number { return data.reduce((a, b) => a + b, 0); // returns 5 for [2, 3] } }

No @Get, no HTTP server, no Express. Just a pattern that maps to a handler.

Key difference from HTTP controllers

HTTP controllers bind to URL paths and HTTP verbs. Microservice controllers bind to message patterns and transports. When a client calls client.send({ cmd: 'add' }, [2, 3]), NestJS serializes the payload to JSON (or Protobuf in gRPC), sends it over the wire, and the matching @MessagePattern handler deserializes and processes it. The response travels back the same way.

This separation means you can swap the transport without touching your business logic. A handler working over TCP today can run over RabbitMQ tomorrow with a config change in main.ts.

@MessagePattern vs @EventPattern

These two decorators cover almost every communication need:

DecoratorClient methodGets a reply?Typical use
@MessagePatternclient.send()Yes (Observable)Fetch user data, calculate sum
@EventPatternclient.emit()NoLog order created, notify warehouse

client.send() returns an RxJS Observable. You await it with .toPromise() or firstValueFrom(). client.emit() returns void, it fires and does not wait.

typescript
// API gateway using both patterns @Get('sum') async sum() { return this.mathClient .send({ cmd: 'add' }, [1, 2, 3]) // waits for reply .toPromise(); } @Post('order') async order(@Body() body: CreateOrderDto) { this.orderClient.emit('order_created', body); // fire and forget return { status: 'queued' }; }

Transport options

Choosing a transport means choosing a tradeoff between latency, throughput, and delivery guarantees.

TransportLatencyThroughputDeliveryBest for
TCPLowMediumAt-most-onceInternal RPC, simple setups
RedisLowHighAt-most-oncePub/sub, caching-adjacent events
NATSVery lowVery highAt-least-onceReal-time fan-out, high volume
RabbitMQMediumHighExactly-once (with ACKs)Order queues, anything needing ACK
KafkaMediumExtremeAt-least-onceEvent logs, replayable streams, >1M msg/sec
gRPCVery lowHighAt-most-onceType-safe APIs, Protobuf, polyglot systems

Start with TCP when splitting a monolith and keeping everything inside one network. Move to Kafka or RabbitMQ when you need durability and replay.

How it works internally

NestJS creates a Server instance per transport. For TCP that is a Node.js net.Server bound to the configured port. When a message arrives, NestJS deserializes the buffer (JSON by default), matches the pattern against registered handlers via its internal MessageBroker, and calls the handler. The response serializes back through the same connection.

Node's event loop handles everything async, no threads involved. For streaming transports like Kafka and NATS, handlers can return RxJS Observables, which NestJS pipes back to the client as a stream.

gRPC is a separate case: it requires .proto files for code generation. Pattern registration and handler lookup still work the same way, but payloads go through Protobuf encoding instead of JSON. Benchmarks from NestJS performance tests show roughly 7x throughput improvement over JSON/TCP for binary-heavy payloads.

Hybrid application

Sometimes you need HTTP and a message transport in the same process. For example, an API gateway that exposes REST to the outside world but also listens on Redis for internal events.

typescript
// main.ts async function bootstrap() { const app = await NestFactory.create(AppModule); // HTTP server app.connectMicroservice({ transport: Transport.REDIS, options: { host: 'localhost', port: 6379 }, }); await app.startAllMicroservices(); // start Redis listener await app.listen(3000); // start HTTP server }

Both run in the same NestJS instance. Controllers with @Get handle HTTP. Controllers with @MessagePattern handle Redis messages. One common mistake here: passing transport options directly to NestFactory.create instead of calling connectMicroservice. The TCP option gets silently ignored that way.

Common mistakes

Using @Get in a microservice:

typescript
@Get('add') // ignored in TCP mode - no Express, no route resolution add() { ... }

Fix: @MessagePattern({ cmd: 'add' }).

Forgetting to ACK in RabbitMQ:

typescript
@MessagePattern('process') process(@Payload() data: any) { doWork(data); // no ack - message redelivers on crash, infinite loop }

This is the number one RabbitMQ issue in NestJS projects. Fix: inject @Ctx() ctx: RmqContext and call ctx.getChannelRef().ack(ctx.getMessage()) after your work completes. Also set noAck: false in bootstrap options.

typescript
@MessagePattern('process') process(@Payload() data: any, @Ctx() ctx: RmqContext) { doWork(data); ctx.getChannelRef().ack(ctx.getMessage()); // RabbitMQ: message handled, do not redeliver }

Hybrid app without connectMicroservice:

typescript
// Wrong: creates only an HTTP server, TCP option is ignored const app = await NestFactory.create(AppModule, { transport: Transport.TCP });

Use app.connectMicroservice({...}) after NestFactory.create.

Sending Buffers over JSON transport:

typescript
client.send({ cmd: 'upload' }, Buffer.from(imageData)); // corrupts data

JSON.stringify does not handle Buffers correctly. Use a custom serializer or switch to gRPC with Protobuf for binary payloads.

Shared state across instances: Two instances of the same microservice with a local cache will get out of sync under load. Keep handlers stateless. Put state in Redis, a database, or a message store.

Real-world usage

  • Internal RPC: user-service and order-service on TCP inside a Kubernetes cluster, no public exposure.
  • E-commerce order flow: order-service emits order_created to RabbitMQ; notification-service and inventory-service both consume it independently.
  • Event log: ride-hailing apps log every driver location update as a Kafka event. Services replay the log to rebuild state after restarts.
  • gRPC gateway: an API gateway proxies typed requests from mobile clients to internal services via Protobuf, similar to how Google and Netflix structure internal comms.
  • Strangler migration: run HTTP and TCP on the same app via connectMicroservice, then peel off services one by one as each proves stable in production.

Follow-up questions

Q: What is the difference between @MessagePattern and @EventPattern?
A: @MessagePattern is request-response, the client awaits a reply. @EventPattern is fire-and-forget, the client emits and moves on, no reply expected. Use @MessagePattern for queries and commands needing confirmation; use @EventPattern for notifications.

Q: How does NestJS handle transport failures?
A: Configure retryAttempts and retryDelay in client options. Under the hood NestJS applies RxJS retryWhen on the Observable. For persistent delivery you still need a broker that supports ACKs, like RabbitMQ or Kafka.

Q: How do you run HTTP and a microservice transport at the same time?
A: Use NestFactory.create for HTTP, then app.connectMicroservice({...}) for the second transport, then call app.startAllMicroservices() before app.listen(). Both run in the same process. NestJS routes HTTP to @Get/@Post handlers and messages to @MessagePattern handlers.

Q: When does the TCP vs gRPC difference actually matter in production?
A: For internal JSON payloads at moderate load, TCP is simpler and sufficient. gRPC matters when payloads are binary-heavy, when you need strict contract enforcement via .proto files, or when talking to services in other languages. Protobuf encoding runs about 7x faster than JSON for large binary messages.

Q: Design a resilient order service with Kafka. How do you handle out-of-order events and ensure exactly-once processing?
A: Partition Kafka topics by orderId so all events for one order go to the same partition in sequence. Use consumer group IDs to avoid duplicate processing across instances. For exactly-once semantics, enable Kafka transactions and use idempotent handlers: store a processed event UUID in Redis before acting, skip duplicates on retry. Handle out-of-order events by versioning the order state and rejecting events with an older version number than what is stored.

Examples

Basic TCP microservice

A minimal setup: one service handles math, one gateway calls it over HTTP.

typescript
// math-service/math.controller.ts import { Controller } from '@nestjs/common'; import { MessagePattern, Payload } from '@nestjs/microservices'; @Controller() export class MathController { @MessagePattern({ cmd: 'sum' }) sum(@Payload() numbers: number[]): number { return numbers.reduce((acc, n) => acc + n, 0); } }
typescript
// api-gateway/gateway.controller.ts import { Controller, Get, Inject } from '@nestjs/common'; import { ClientProxy } from '@nestjs/microservices'; import { firstValueFrom } from 'rxjs'; @Controller('math') export class GatewayController { constructor(@Inject('MATH_SERVICE') private client: ClientProxy) {} @Get('sum') async sum() { const result = await firstValueFrom( this.client.send({ cmd: 'sum' }, [1, 2, 3, 4, 5]) ); return { result }; // { result: 15 } } }

Register MATH_SERVICE in ClientsModule.register([{ name: 'MATH_SERVICE', transport: Transport.TCP, options: { host: 'localhost', port: 3001 } }]). The gateway stays HTTP; the math service is pure TCP.

RabbitMQ with manual ACK (e-commerce order flow)

Order-service publishes, user-service consumes and acknowledges manually to avoid infinite redelivery on crash.

typescript
// user-service/user.controller.ts import { Controller } from '@nestjs/common'; import { MessagePattern, Payload, Ctx, RmqContext } from '@nestjs/microservices'; @Controller() export class UserController { @MessagePattern('user_create') async create( @Payload() data: { email: string }, @Ctx() ctx: RmqContext, ) { const channel = ctx.getChannelRef(); const originalMsg = ctx.getMessage(); try { const user = await this.userService.create(data.email); channel.ack(originalMsg); // done, do not redeliver return { id: user.id, email: user.email }; } catch (err) { channel.nack(originalMsg, false, true); // requeue on failure throw err; } } }
typescript
// user-service/main.ts bootstrap config { transport: Transport.RMQ, options: { urls: ['amqp://localhost:5672'], queue: 'user_queue', noAck: false, // required for manual ACK to work }, }

Without noAck: false and the explicit channel.ack, every crash redelivers the message indefinitely.

gRPC streaming (senior-level)

gRPC allows streaming responses, not just single replies. This requires a .proto file and the @grpc/grpc-js package.

protobuf
// math.proto syntax = "proto3"; package math; service MathService { rpc StreamNumbers(NumbersRequest) returns (stream NumberResponse); } message NumbersRequest { repeated int32 numbers = 1; } message NumberResponse { int32 result = 1; }
typescript
// math.controller.ts import { Controller } from '@nestjs/common'; import { GrpcMethod } from '@nestjs/microservices'; import { Observable, from } from 'rxjs'; import { map } from 'rxjs/operators'; @Controller() export class MathController { @GrpcMethod('MathService', 'StreamNumbers') streamNumbers(data: { numbers: number[] }): Observable<{ result: number }> { // emits one response per number, streams back to client return from(data.numbers).pipe( map(n => ({ result: n * n })) ); } }

Common pitfall: forgetting to install @grpc/grpc-js or misconfiguring the protoPath gives a silent "method not found" error at runtime, not at compile time. Check the package and the protoPath in your GrpcOptions first.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?