NATS + .NET: The Simplest Messaging Setup I have Ever Done

NATS + .NET: The Simplest Messaging Setup I have Ever Done
SHARE

I Tried NATS with .NET and Honestly, Why Didn't I Do This Sooner?

Look, I've been around the block with message brokers. RabbitMQ, Kafka, Azure Service Bus... you name it, I've probably spent a weekend cursing at configuration files trying to get it working. So when a friend who works at an IoT company mentioned NATS, I was skeptical. "Another messaging system to learn," I thought. "Great."

But I had some free time over the weekend and figured I'd give it a shot. Here's the thing: I was wrong to be skeptical.

The Setup That Made Me Do a Double-Take

I'm not exaggerating when I say the NATS server was running in under a minute. One Docker command:

docker run -p 4222:4222 nats:latest

And... that's it for getting a local NATS server up and running. No YAML files to craft. No environment variables to hunt down. No "wait, which port was it again?" moments. The server just started. I actually refreshed my terminal thinking something had gone wrong because it was too fast.

Coming from setups where I'd budget half a day just to get a local broker running properly, this felt almost suspicious. Where's the catch?

Getting .NET Talking to NATS

Alright, so the server was easy. But surely the client library would be where things get complicated, right?

Nope.

dotnet add package NATS.Net

Then I wrote my first publisher:

await using var client = new NatsClient("localhost:4222");

await client.PublishAsync("orders.created", new Order 
{ 
    Id = Guid.NewGuid(), 
    Product = "Widget", 
    Quantity = 5 
});

I kept waiting for the other shoe to drop. Where's the connection factory? The channel configuration? The exchange bindings?

There aren't any. You connect, you publish. Done.

Subscribing Felt Equally Weird (In a Good Way)

Here's the subscriber code:

await using var client = new NatsClient("localhost:4222");

await foreach (var msg in client.SubscribeAsync<Order>("orders.created"))
{
    Console.WriteLine($"Got an order: {msg.Data.Quantity} x {msg.Data.Product}");
}

The await foreach pattern just clicked for me. It feels like modern C#, not some awkward callback soup from 2010. Messages come in, you process them, life goes on.

The "Aha" Moment

About twenty minutes into my tinkering, I had two console apps talking to each other through NATS. Twenty minutes. I remember spending longer than that just figuring out connection strings for other brokers.

That's when it hit me. NATS isn't trying to be everything to everyone by default. It focuses on fast, simple messaging first, and gets out of your way instead of leading with complexity.

Now I understand why my friend's IoT company uses it. When you've got thousands of devices sending small messages constantly, you need something lightweight and fast. NATS makes total sense for that.

Request-Reply Without the Headache

I wanted to try a quick request-reply pattern, where one service asks another a question and waits for an answer. In some systems, this means setting up reply queues, correlation IDs, and a bunch of plumbing code.

In NATS:

// The service answering questions
await foreach (var msg in client.SubscribeAsync<StockQuery>("inventory.check"))
{
    var stock = await CheckInventory(msg.Data.ProductId);
    await msg.ReplyAsync(new StockResponse { Available = stock });
}

// The service asking
var response = await client.RequestAsync<StockQuery, StockResponse>(
    "inventory.check", 
    new StockQuery { ProductId = "WIDGET-001" }
);

It just works. The library handles the reply routing internally. I didn't have to think about it.

What I Genuinely Appreciated

It respects your time. The learning curve is maybe 30 minutes to be productive - enough to publish, subscribe, and understand message flow without wading through documentation.

The defaults are sane. I didn’t need to tune anything to publish and consume messages locally. When I eventually need to optimize, those options exist, but they’re not shoved in my face on day one.

It feels native to .NET. The client library leans into modern patterns like async/await and IAsyncEnumerable, and the APIs follow conventions that feel intentional rather than adapted from another ecosystem.

It’s fast. Fast in a way you can feel while developing. Messages move with no perceptible delay, which keeps the focus on code rather than infrastructure.

The Bottom Line

What started as a lazy weekend experiment turned into a genuine discovery. I went in expecting another configuration marathon. Instead, I got a messaging system that let me focus on actually writing code instead of fighting infrastructure.

Important context: everything shown above uses core NATS in its simplest form. This means messages are ephemeral, delivered only to active subscribers, and lost on server restarts. There is no persistence, replay, or delivery guarantee in this setup, and the Docker configuration shown is intended for local development and experimentation.

For real production use, you would typically reuse long-lived connections, add authentication and TLS, and choose whether you need stronger guarantees via JetStream, which introduces persistence, replay, and more operational considerations.

That said, as a starting point, core NATS is refreshingly straightforward. If you've been curious about NATS, spin it up one afternoon to understand the fundamentals. Worst case, you lose an hour. Best case, you discover a messaging system that gets out of your way.

I know I did.