---
id: 8f79af5c6af89b748112aa8b77b348ab
author: prravda
title: Embedded NATS Server Configuration and In-Process Communication Architecture in Go Applications
description: This document covers how to configure and run a NATS broker (nats-server/v2) embedded within a Go process, as well as in-memory communication based on InProcessConn and the design of consumer-defined interfaces.
language: en
date: 2025-08-04T01:05:22.150350794Z
path: /blog/posts/how-embedded-nats-communicate-with-go-application-z36089af0
category: network
---

# Getting started

## About NATS

> Software applications and services need to exchange data. NATS is an infrastructure that allows such data exchange, segmented in the form of messages. We call this a "**message oriented middleware**".
>
> With NATS, application developers can:
>
> - Effortlessly build distributed and scalable client-server applications.
> - Store and distribute data in realtime in a general manner. This can flexibly be achieved across various environments, languages, cloud providers and on-premises systems.
>
> [What is NATS, NATS docs](https://docs.nats.io/nats-concepts/what-is-nats)

- NATS is a message broker written in Go.

## Embedded NATS

> If your application is in Go, and if it fits your use case and deployment scenarios, you can even embed a NATS server inside your application.

[Embedding NATS, NATS docs](https://docs.nats.io/running-a-nats-service/clients#embedding-nats)

>

- Additionally, NATS has a distinctive feature: for applications written in Go, it supports embedded mode.
- In other words, rather than the common message broker approach of running a separate broker server and communicating with it through client connections from the application, the broker itself can be embedded within an application written in Go.

## Benefits and use cases of embedded NATS

- Since there is a well-explained [YouTube video](https://youtu.be/cdTrl8UfcBo?si=KYcXOQpiyLUft6AN), the video link will serve in its place.
- Without deploying a separate message broker server, one can build a modular monolith application to achieve separation of concerns while leveraging the advantage of embedding NATS. In addition, single binary deployment becomes possible.
- It can be usefully employed not only in platforms with no network (WASM) but also in offline-first applications.

# Example on official docs

```go
package main

import (
    "fmt"
    "time"

    "github.com/nats-io/nats-server/v2/server"
    "github.com/nats-io/nats.go"
)

func main() {
    opts := &server.Options{}

    // Initialize new server with options
    ns, err := server.NewServer(opts)

    if err != nil {
        panic(err)
    }

    // Start the server via goroutine
    go ns.Start()

    // Wait for server to be ready for connections
    if !ns.ReadyForConnections(4 * time.Second) {
        panic("not ready for connection")
    }

    // Connect to server
    nc, err := nats.Connect(ns.ClientURL())

    if err != nil {
        panic(err)
    }

    subject := "my-subject"

    // Subscribe to the subject
    nc.Subscribe(subject, func(msg *nats.Msg) {
        // Print message data
        data := string(msg.Data)
        fmt.Println(data)

        // Shutdown the server (optional)
        ns.Shutdown()
    })

    // Publish data to the subject
    nc.Publish(subject, []byte("Hello embedded NATS!"))

    // Wait for server shutdown
    ns.WaitForShutdown()
}
```

- This is the [Embedded NATS example](https://dev.to/karanpratapsingh/embedding-nats-in-go-19o) linked from the official NATS documentation, but if you proceed with the example code as is, communication does not take place in embedding mode.

```bash
Every 2.0s: netstat -an | grep 127.0.0.1         pravdalaptop-home.local: 02:34:20
                                                                     in 0.017s (0)
...
tcp4       0      0  127.0.0.1.4222         127.0.0.1.63769        TIME_WAIT
```

- Using the `watch 'netstat -an | grep 127.0.0.1'` command to monitor network traffic going to and from [localhost](http://localhost) (127.0.0.1), while running the Go file with `go run .`, you can see new network requests originating from port `4222`, NATS's default port, being added.

# Right configurations for embedding mode

- To communicate in embedded mode as intended, the following two options are required.
  - Client: the `InProcessServer` option must be provided.
  - Server: the `DontListen` flag in `Server.Options` must be explicitly set to `true`.
- These parts are not officially documented, and the origin of this feature can be traced through this [PR](https://github.com/nats-io/nats-server/pull/2360).

  > This PR adds three things:
  >
  > 1. `InProcessConn()` function to `Server` which builds a `net.Pipe` to get a connection to the NATS server without using TCP sockets
  > 2. `DontListen` option which tells the NATS server not to listen on the usual TCP listener
  > 3. `startupComplete` channel, which is closed right before we start `AcceptLoop`, and `readyForConnections` will wait for it
  >
  > The main motivation for this is that we have an application that can run either in a monolith (single-process) mode or a polylith (multi-process) mode. We'd like to be able to use NATS for both modes for simplicity, but the monolith mode has to be able to cater for a variety of platforms where opening socket connections either doesn't make sense (mobile) or just isn't possible (WASM). These changes will allow us to use NATS entirely in-process instead.
  >
  > An accompanying PR [nats-io/nats.go#774](https://github.com/nats-io/nats.go/pull/774) adds support to the client side.
  >
  > This is my first PR to this project so apologies in advance if I've missed anything obvious anywhere.
  >
  > /cc @nats-io/core

# Working Example for embedded mode

```go
package main

import (
	"fmt"
	"time"

	"github.com/nats-io/nats-server/v2/server"
	"github.com/nats-io/nats.go"
)

func main() {
	opts := &server.Options{
		// for configuring the embeded NATS server
		// set DonListen as true
		DontListen: true,
	}

	// Initialize new server with options
	ns, err := server.NewServer(opts)

	if err != nil {
		panic(err)
	}

	// Start the server via goroutine
	go ns.Start()

	// Wait for server to be ready for connections
	if !ns.ReadyForConnections(10 * time.Second) {
		panic("not ready for connection")
	}

	// Connect to server via in-process connection
	nc, err := nats.Connect(ns.ClientURL(), nats.InProcessServer(ns))

	if err != nil {
		panic(err)
	}

	subject := "my-subject"

	// Subscribe to the subject
	nc.Subscribe(subject, func(msg *nats.Msg) {
		// Print message data
		data := string(msg.Data)
		fmt.Println(data)

		// Shutdown the server (optional)
		ns.Shutdown()
	})

	// Publish data to the subject
	nc.Publish(subject, []byte("Hello embedded NATS!"))

	// Wait for server shutdown
	ns.WaitForShutdown()
}

```

```bash
Every 2.0s: netstat -an | grep 127.0.0.1         pravdalaptop-home.local: 02:37:50
                                                                     in 0.023s (0)
...no additional logs

```

- Now you can see that no additional network hop occurs, as intended.

# Under the hood

## TL;DR

![diagram1](/assets/images/embedded-nats/seq1.svg)

- This is a sequence diagram showing which functions operate and how they behave internally when the above code is run from `main.go`, and the key points are summarized below.
  - With `DontListen: true`, the server skips the client listening phase called `AcceptLoop`.
  - If `InProcessServer` is enabled among the client's Connect options, an in-memory connection is created, a pipe is made via `net.Pipe`, and the end of the pipe is returned to the client as a `net.Conn` type.
  - The client and server carry out in-process communication through this connection.

## Server

### AcceptLoop

```go
// nats-server/server/server.go

// Wait for clients.
if !opts.DontListen {
	s.AcceptLoop(clientListenReady)
}
```

- First, when `DontListen` is true, the client listening phase called `AcceptLoop` is skipped.

```go
// nats-server/server/server.go

// AcceptLoop is exported for easier testing.
func (s *Server) AcceptLoop(clr chan struct{}) {
	// If we were to exit before the listener is setup properly,
	// make sure we close the channel.
	defer func() {
		if clr != nil {
			close(clr)
		}
	}()

	if s.isShuttingDown() {
		return
	}

	// Snapshot server options.
	opts := s.getOpts()

	// Setup state that can enable shutdown
	s.mu.Lock()
	hp := net.JoinHostPort(opts.Host, strconv.Itoa(opts.Port))
	l, e := natsListen("tcp", hp)
	s.listenerErr = e
	if e != nil {
		s.mu.Unlock()
		s.Fatalf("Error listening on port: %s, %q", hp, e)
		return
	}
	s.Noticef("Listening for client connections on %s",
		net.JoinHostPort(opts.Host, strconv.Itoa(l.Addr().(*net.TCPAddr).Port)))

	// Alert of TLS enabled.
	if opts.TLSConfig != nil {
		s.Noticef("TLS required for client connections")
		if opts.TLSHandshakeFirst && opts.TLSHandshakeFirstFallback == 0 {
			s.Warnf("Clients that are not using \"TLS Handshake First\" option will fail to connect")
		}
	}

	// If server was started with RANDOM_PORT (-1), opts.Port would be equal
	// to 0 at the beginning this function. So we need to get the actual port
	if opts.Port == 0 {
		// Write resolved port back to options.
		opts.Port = l.Addr().(*net.TCPAddr).Port
	}

	// Now that port has been set (if it was set to RANDOM), set the
	// server's info Host/Port with either values from Options or
	// ClientAdvertise.
	if err := s.setInfoHostPort(); err != nil {
		s.Fatalf("Error setting server INFO with ClientAdvertise value of %s, err=%v", opts.ClientAdvertise, err)
		l.Close()
		s.mu.Unlock()
		return
	}
	// Keep track of client connect URLs. We may need them later.
	s.clientConnectURLs = s.getClientConnectURLs()
	s.listener = l

	go s.acceptConnections(l, "Client", func(conn net.Conn) { s.createClient(conn) },
		func(_ error) bool {
			if s.isLameDuckMode() {
				// Signal that we are not accepting new clients
				s.ldmCh <- true
				// Now wait for the Shutdown...
				<-s.quitCh
				return true
			}
			return false
		})
	s.mu.Unlock()

	// Let the caller know that we are ready
	close(clr)
	clr = nil
}
```

- For reference, the AcceptLoop function carries out the following processes. The parts related to network communication, such as `TLS` and `hostPort`, are unnecessary for in-process communication and can therefore be omitted.

## Client

### InProcessServer

```go

// nats-go/nats.go

// Connect will attempt to connect to the NATS system.
// The url can contain username/password semantics. e.g. nats://derek:pass@localhost:4222
// Comma separated arrays are also supported, e.g. urlA, urlB.
// Options start with the defaults but can be overridden.
// To connect to a NATS Server's websocket port, use the `ws` or `wss` scheme, such as
// `ws://localhost:8080`. Note that websocket schemes cannot be mixed with others (nats/tls).
func Connect(url string, options ...Option) (*Conn, error) {
	opts := GetDefaultOptions()
	opts.Servers = processUrlString(url)
	for _, opt := range options {
		if opt != nil {
			if err := opt(&opts); err != nil {
				return nil, err
			}
		}
	}
	return opts.Connect()
}
```

```go
// nats-go/nats.go

// Options can be used to create a customized connection.
type Options struct {
	// Url represents a single NATS server url to which the client
	// will be connecting. If the Servers option is also set, it
	// then becomes the first server in the Servers array.
	Url string

	// InProcessServer represents a NATS server running within the
	// same process. If this is set then we will attempt to connect
	// to the server directly rather than using external TCP conns.
	InProcessServer InProcessConnProvider

	//...
}
```

```go
// nats-go/nats.go

type InProcessConnProvider interface {
	InProcessConn() (net.Conn, error)
}
```

- The `Connect` function, which establishes the connection between the NATS server and the NATS client, accepts a client URL and connect Options, and the Options struct that gathers these settings has a field named `InProcessServer` of the `InProcessConnProvider` interface type.

```go
// main.go of example code

// Initialize new server with options
ns, err := server.NewServer(opts)

//...

// Connect to server via in-process connection
nc, err := nats.Connect(ns.ClientURL(), nats.InProcessServer(ns))
```

- When the NATS client calls Connect and passes `nats.InProcessServer(ns)`,

```go
// nats-go/nats.go

// InProcessServer is an Option that will try to establish a direction to a NATS server
// running within the process instead of dialing via TCP.
func InProcessServer(server InProcessConnProvider) Option {
	return func(o *Options) error {
		o.InProcessServer = server
		return nil
	}
}
```

- the InProcessServer field of the option is replaced with the embedded NATS server,

```go
// nats-go/nats.go

// createConn will connect to the server and wrap the appropriate
// bufio structures. It will do the right thing when an existing
// connection is in place.
func (nc *Conn) createConn() (err error) {
	if nc.Opts.Timeout < 0 {
		return ErrBadTimeout
	}
	if _, cur := nc.currentServer(); cur == nil {
		return ErrNoServers
	}

	// If we have a reference to an in-process server then establish a
	// connection using that.
	if nc.Opts.InProcessServer != nil {
		conn, err := nc.Opts.InProcessServer.InProcessConn()
		if err != nil {
			return fmt.Errorf("failed to get in-process connection: %w", err)
		}
		nc.conn = conn
		nc.bindToNewConn()
		return nil
	}

	//...
}
```

- In the `createConn` function, which creates the connection, when the `InProcessServer` option is not nil (that is, valid), the interface runs the `InProcessConn` of the InProcessServer in the option,

```go
// nats-server/server/server.go

// InProcessConn returns an in-process connection to the server,
// avoiding the need to use a TCP listener for local connectivity
// within the same process. This can be used regardless of the
// state of the DontListen option.
func (s *Server) InProcessConn() (net.Conn, error) {
	pl, pr := net.Pipe()
	if !s.startGoRoutine(func() {
		s.createClientInProcess(pl)
		s.grWG.Done()
	}) {
		pl.Close()
		pr.Close()
		return nil, fmt.Errorf("failed to create connection")
	}
	return pr, nil
}
```

- This invokes the `InProcessConn` implemented on the server.
- This function is called from `nats.go`, the NATS Go client, when the `InProcessServer` of nc (the NATS connection) is not nil, creating a connection (`net.Conn`) that is then bound to the server's connection.

# Consumer driven interface of Go

> A type implements an interface by implementing its methods. There is no explicit declaration of intent, no "implements" keyword. Implicit interfaces decouple the definition of an interface from its implementation, which could then appear in any package without prearrangement.

[Interfaces are implemented implicitly, A Tour of Go](https://go.dev/tour/methods/10)

> If a type exists only to implement an interface and will never have exported methods beyond that interface, there is no need to export the type itself.

[Generality, Effective Go](https://go.dev/doc/effective_go#generality)

- This interface design well embodies what is commonly referred to in Go as a consumer defined interface and structural typing (duck typing), so I would like to introduce this topic as well.

```go
// nats-go/nats.go

// Options can be used to create a customized connection.
type Options struct {
	// Url represents a single NATS server url to which the client
	// will be connecting. If the Servers option is also set, it
	// then becomes the first server in the Servers array.
	Url string

	// InProcessServer represents a NATS server running within the
	// same process. If this is set then we will attempt to connect
	// to the server directly rather than using external TCP conns.
	InProcessServer InProcessConnProvider

	//...
}
```

```go
// nats-go/nats.go

type InProcessConnProvider interface {
	InProcessConn() (net.Conn, error)
}
```

- Returning to the code, the `InProcessServer` option struct field in the nats.go client is defined with the `InProcessConnProvider` interface, which performs only `InProcessConn`.

```go
// nats-server/server/server.go

// InProcessConn returns an in-process connection to the server,
// avoiding the need to use a TCP listener for local connectivity
// within the same process. This can be used regardless of the
// state of the DontListen option.
func (s *Server) InProcessConn() (net.Conn, error) {
	pl, pr := net.Pipe()
	if !s.startGoRoutine(func() {
		s.createClientInProcess(pl)
		s.grWG.Done()
	}) {
		pl.Close()
		pr.Close()
		return nil, fmt.Errorf("failed to create connection")
	}
	return pr, nil
}
```

- However, the type passed into it is the `Server` of nats-server, which performs a variety of functions beyond `InProcessConn`.
- This is because, in this situation, the client's only concern is whether the `InProcessConn` interface is provided or not, and the others are not of much importance.
- Therefore, the nats.go client uses only the `InProcessConnProvider` consumer defined interface, which defines just the `InProcessConn() (net.Conn, error)` capability.

# Conclusion

- We have briefly covered NATS's embedded mode and how it works, along with the Go consumer defined interface that can be seen through NATS's code.
- I hope this information will be helpful to those who use NATS for the same purposes, and I will conclude this post here.
