Skip to content
NestJS ns microservices 4 min read

TCP Transport

The TCP transporter is the default transport layer in NestJS microservices. It opens a raw TCP socket between a client and a server and exchanges length-prefixed JSON packets over it. Because it ships built into @nestjs/microservices with zero external dependencies, it is the fastest way to split a monolith into independently deployable services or to experiment with the microservice model before introducing a message broker like Redis, NATS, or Kafka.

Creating a TCP microservice

A microservice is bootstrapped with NestFactory.createMicroservice instead of NestFactory.create. You pass the root module plus a transport configuration object that declares the transporter and its options. For TCP you specify Transport.TCP and supply the host and port the server should bind to.

// 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.TCP,
      options: {
        host: '127.0.0.1',
        port: 3001,
      },
    },
  );

  await app.listen();
  console.log('Math microservice is listening on tcp://127.0.0.1:3001');
}
bootstrap();

Output:

Math microservice is listening on tcp://127.0.0.1:3001

If you omit host and port, NestJS defaults to localhost:3000. In containerised environments set host: '0.0.0.0' so the socket accepts connections from outside the container.

Handling incoming messages

On the server side, controllers expose handlers decorated with @MessagePattern. The pattern is an arbitrary token (string or object) that the client uses to address a specific handler. A @MessagePattern handler implements the synchronous request/response model: whatever it returns is serialized and streamed back to the caller.

// math.controller.ts
import { Controller } from '@nestjs/common';
import { MessagePattern, Payload } from '@nestjs/microservices';

@Controller()
export class MathController {
  @MessagePattern({ cmd: 'sum' })
  accumulate(@Payload() data: number[]): number {
    return data.reduce((acc, value) => acc + value, 0);
  }
}
// app.module.ts
import { Module } from '@nestjs/common';
import { MathController } from './math.controller';

@Module({
  controllers: [MathController],
})
export class AppModule {}

Connecting a client proxy

A consumer talks to the microservice through a ClientProxy. The recommended way to register one is ClientsModule.register, which provides an injectable proxy configured with the same transport and options used by the server. The name you assign becomes the DI token.

// gateway.module.ts
import { Module } from '@nestjs/common';
import { ClientsModule, Transport } from '@nestjs/microservices';
import { GatewayController } from './gateway.controller';

@Module({
  imports: [
    ClientsModule.register([
      {
        name: 'MATH_SERVICE',
        transport: Transport.TCP,
        options: {
          host: '127.0.0.1',
          port: 3001,
        },
      },
    ]),
  ],
  controllers: [GatewayController],
})
export class GatewayModule {}

Inject the proxy and call send() to dispatch a request. send() returns an RxJS Observable; convert it with firstValueFrom when you need a promise.

// 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 readonly client: ClientProxy,
  ) {}

  @Get('sum')
  async getSum(): Promise<number> {
    const result$ = this.client.send<number, number[]>(
      { cmd: 'sum' },
      [1, 2, 3, 4, 5],
    );
    return firstValueFrom(result$);
  }
}

Output:

$ curl http://localhost:3000/math/sum
15

The synchronous request/response model

send() implements request/response semantics: the framework correlates each outbound packet with the reply that comes back over the same socket, so the Observable emits exactly the value the handler returned, then completes. This is conceptually synchronous even though the I/O is non-blocking — the caller awaits a single, correlated answer. Use it for queries where you need the result. For fire-and-forget notifications, use emit() with an @EventPattern handler instead, which returns immediately and expects no reply.

TCP options reference

OptionTypeDefaultDescription
hoststring'localhost'Address the server binds to / client dials.
portnumber3000TCP port for the socket.
retryAttemptsnumber0How many times the client retries a failed connection.
retryDelaynumber0Milliseconds between connection retries.
serializerSerializerJSONCustom outbound packet serializer.
deserializerDeserializerJSONCustom inbound packet deserializer.
socketClassType<TcpSocket>built-inOverride the underlying socket implementation.

The client establishes its socket lazily on the first send()/emit(). Call await client.connect() during startup if you want to fail fast when the server is unreachable.

Best practices

  • Bind to 0.0.0.0 inside containers and to 127.0.0.1 for local-only services to avoid exposing the socket unintentionally.
  • Set retryAttempts and retryDelay on the client so transient restarts of the server do not crash the gateway.
  • Keep message payloads serializable plain objects — TCP packets are JSON encoded, so class instances lose their prototype across the wire.
  • Use structured patterns like { cmd: 'sum' } rather than bare strings; they scale better as the number of handlers grows.
  • Prefer firstValueFrom over manually subscribing so requests resolve as awaitable promises and errors propagate cleanly.
  • Treat TCP as a great default for internal service-to-service calls, but move to a broker-backed transporter once you need durability, fan-out, or back-pressure.
Last updated June 14, 2026
Was this helpful?