Skip to main content
SEMastery

Getting Started With NServiceBus in .NET: A Beginner's Guide

Learn NServiceBus in .NET from scratch: endpoints, commands, events, handlers, retries, and pub-sub. Simple words, real-life examples, code, and diagrams.

15 min readUpdated November 19, 2025

A post office for your code

Think about how a big railway station in India works. Thousands of people need to get messages and parcels to each other. Nobody walks up to a stranger and shouts their message across the platform. Instead, there is a parcel counter. You hand your parcel to the counter, with a clear address on it. The staff make sure it reaches the right place. If the train is late, the parcel waits safely. If one delivery fails, they try again.

You do not need to know which train it goes on. You do not need to wait at the counter. You drop the parcel and walk away, trusting the system.

NServiceBus is the parcel counter for your .NET code. Different parts of your application hand messages to it, with a clear address. NServiceBus makes sure each message reaches the right handler. If a service is busy or down, the message waits in a queue. If processing fails, it tries again. You write the simple part. NServiceBus does the hard, reliable part.

Let us learn how it works, step by step, in plain words.

Why not just call the method directly?

Imagine a small online shop built in the usual way. When an order comes in, your code might do this:

public async Task PlaceOrder(Order order)
{
    await _orderRepository.SaveAsync(order);
    await _emailService.SendConfirmationAsync(order);   // calls another service directly
    await _warehouseService.ReserveStockAsync(order);   // and another one
    await _loyaltyService.AddPointsAsync(order);         // and another one
}

This looks fine on a calm day. But think about the bad days:

  • If the email service is slow, the customer waits for the whole thing.
  • If the warehouse service is down, the whole order fails, even though the order was already saved.
  • If you add a fifth service later, you must edit this method again.

Everything is glued together tightly. One slow or broken part hurts everyone. This is called tight coupling, and it makes systems fragile.

Messaging fixes this. Instead of calling each service directly, your code drops a message into a queue and moves on. Each service picks up its own message when it is ready. If one is slow, the others are not affected. This is loose coupling, and it is what NServiceBus gives you.

Direct calls glue everything together. Messaging lets each service work on its own.

The main ideas, in plain words

NServiceBus has a small set of words. Once you know these five, the rest is easy.

WordWhat it meansEveryday picture
MessageA small piece of data you sendA parcel
EndpointA running program that sends or receives messagesA post office branch
HandlerA class that runs when a message arrivesThe clerk who opens the parcel
TransportThe queue technology that carries messagesThe trains and trucks
PersistenceWhere NServiceBus stores its bookkeepingThe filing cabinet

An endpoint is the heart of it. It is just a normal .NET process, like a console app or a web app, that has NServiceBus switched on inside it. An endpoint can send messages, receive messages, or both. When the endpoint starts, NServiceBus scans your code, finds all your handler classes, and wires them up automatically. You never register handlers by hand.

Commands and events: the two kinds of messages

This is the most important idea, so let us go slow. There are two kinds of messages, and they feel very different.

A command is an instruction. It says, "please do this." Like PlaceOrder or ChargeCard. A command is bossy. It points at one service and expects it to act. A command has exactly one handler. You use Send to deliver a command.

An event is an announcement. It says, "this already happened." Like OrderPlaced or PaymentReceived. Notice the past tense. An event is polite. It does not tell anyone what to do. It just shares news. Many services can listen to the same event, or none at all. You use Publish to share an event.

Here is a simple way to remember it: a command is like sending one friend a WhatsApp message asking them to buy milk. An event is like posting "I just reached home!" in a group, where anyone who cares can react.

CommandEvent
Meaning"Do this""This happened"
TensePresent (PlaceOrder)Past (OrderPlaced)
HandlersExactly oneZero or many
API usedSendPublish
Who decides the receiverThe senderThe subscribers

How a command flows

Sender
Queue
Handler
Done

Steps

1

Sender

Calls Send(PlaceOrder)

2

Queue

Command waits safely

3

Handler

One handler runs

4

Done

Order created

One sender, one receiver, a clear instruction.

Setting up your first endpoint

Let us build a tiny endpoint. We will use the Learning Transport, which stores messages as files on your disk. It needs no install and is perfect for practice. Later you swap it for RabbitMQ or Azure Service Bus with one line.

First, create a console app and add the package:

// In a terminal:
//   dotnet new console -n Shipping
//   dotnet add package NServiceBus
 
using NServiceBus;
 
var builder = Host.CreateApplicationBuilder(args);
 
// Give the endpoint a name. This becomes its queue name.
var endpointConfig = new EndpointConfiguration("Shipping");
 
// Use the Learning Transport for practice (files on disk).
endpointConfig.UseTransport(new LearningTransport());
 
// Tell NServiceBus to host inside the .NET generic host.
builder.UseNServiceBus(endpointConfig);
 
var app = builder.Build();
await app.RunAsync();

That is a complete endpoint. When it starts, NServiceBus creates a queue named Shipping, scans for handlers, and waits for messages. The endpoint name matters: it is the address other endpoints use to send to you.

What happens inside an endpoint when it starts up.

Defining a message

A message is just a plain C# class. It carries data, nothing more. By convention you mark it so NServiceBus knows what kind it is.

// A command: an instruction for one service.
public class PlaceOrder : ICommand
{
    public string OrderId { get; set; } = string.Empty;
    public string ProductCode { get; set; } = string.Empty;
    public int Quantity { get; set; }
}
 
// An event: news that something happened (past tense).
public class OrderPlaced : IEvent
{
    public string OrderId { get; set; } = string.Empty;
}

Using ICommand and IEvent is the friendly way. It makes your intent obvious and lets NServiceBus check that you are using Send for commands and Publish for events. You can also use conventions or attributes, but the marker interfaces are the clearest place to begin.

Keep messages simple and small. They get serialized (turned into bytes) to travel through the queue, so use plain properties. Do not put logic, database connections, or huge objects inside a message.

Writing a handler

A handler is the clerk who opens the parcel. It is a class that implements IHandleMessages<T>, where T is the message type.

public class PlaceOrderHandler : IHandleMessages<PlaceOrder>
{
    private readonly ILogger<PlaceOrderHandler> _logger;
 
    public PlaceOrderHandler(ILogger<PlaceOrderHandler> logger)
    {
        _logger = logger;
    }
 
    public async Task Handle(PlaceOrder message, IMessageHandlerContext context)
    {
        _logger.LogInformation("Placing order {OrderId} for {Qty} x {Product}",
            message.OrderId, message.Quantity, message.ProductCode);
 
        // ... save the order in your database here ...
 
        // Now announce to the world that the order was placed.
        await context.Publish(new OrderPlaced { OrderId = message.OrderId });
    }
}

Notice two things. First, the handler asks for ILogger in its constructor. NServiceBus uses the normal .NET dependency injection, so anything you registered is available here. Second, the IMessageHandlerContext is your toolbox. From it you can Send more commands, Publish events, or Reply to whoever sent the message. Here we publish OrderPlaced so other services can react.

You never write code to find or call this handler. NServiceBus discovers it at startup and runs it whenever a PlaceOrder message arrives.

Sending a command

To send a command, you ask the message session to Send it. In a web app you usually inject IMessageSession.

app.MapPost("/orders", async (PlaceOrder command, IMessageSession messageSession) =>
{
    // Hand the command to NServiceBus and return immediately.
    await messageSession.Send(command);
    return Results.Accepted();
});

The web request finishes fast because it does not wait for the order to be fully processed. It just drops the parcel at the counter. The Shipping endpoint will pick it up and run the handler in the background. If the Shipping endpoint is busy or restarting, the command waits safely in the queue until it is ready.

From web request to background work

HTTP POST
Send
Queue
Handler
Publish event

Steps

1

HTTP POST

User places order

2

Send

Command queued, 202 returned

3

Queue

Waits for Shipping

4

Handler

Order processed

5

Publish event

OrderPlaced shared

The user gets a quick reply while work continues safely behind the scenes.

Publish and subscribe: telling many at once

Now the fun part. When the order handler publishes OrderPlaced, who hears it?

Anyone who subscribed. A subscriber is just another endpoint with a handler for OrderPlaced. For example, an Email endpoint and a Loyalty endpoint can both handle the same event, each doing its own job.

// In the Email endpoint
public class SendConfirmationHandler : IHandleMessages<OrderPlaced>
{
    public Task Handle(OrderPlaced message, IMessageHandlerContext context)
    {
        // send a "thank you for your order" email
        return Task.CompletedTask;
    }
}
 
// In the Loyalty endpoint
public class AddPointsHandler : IHandleMessages<OrderPlaced>
{
    public Task Handle(OrderPlaced message, IMessageHandlerContext context)
    {
        // add reward points to the customer
        return Task.CompletedTask;
    }
}

The publisher does not know these endpoints exist. It just shouts "OrderPlaced!" into the air. Each subscriber catches its own copy. This is the magic of publish-subscribe: you can add a tenth listener next year without touching the order code at all.

By default NServiceBus uses auto-subscribe. When a subscriber endpoint starts, it looks at the events it has handlers for and subscribes to them on its own. You do not manage subscription lists by hand.

One event is published once and delivered to every subscriber.

When things go wrong: automatic retries

Real systems fail. A database may be locked for a moment. A network may blink. NServiceBus is built for this and retries for you, in two stages.

First come immediate retries. The message is tried again right away, a few times. This handles tiny, momentary glitches like a brief lock.

If the message still fails, delayed retries kick in. NServiceBus waits a short while, then tries again, waiting a little longer each round. This gives a struggling database time to recover.

If every retry fails, the message is moved to a separate error queue. This is very important: the bad message steps aside so it does not block all the good messages behind it. Later, once you fix the bug, you can replay the failed messages from the error queue, and they get processed as if nothing happened.

StageWhen it runsWhat it is good for
Immediate retriesRight away, a few timesTiny glitches, brief locks
Delayed retriesAfter a growing waitDatabase recovering, service restarting
Error queueAfter all retries failReal bugs you must fix, then replay
The path a failing message takes through the retry stages.

You configure these retries in a few lines, and the defaults are sensible:

var recoverability = endpointConfig.Recoverability();
 
recoverability.Immediate(immediate => immediate.NumberOfRetries(3));
 
recoverability.Delayed(delayed =>
{
    delayed.NumberOfRetries(2);
    delayed.TimeIncrease(TimeSpan.FromSeconds(10));
});

Choosing a transport

The transport is the queue technology under the hood. NServiceBus does not lock you into one. You write the same handlers and the same Send/Publish calls, and only the configuration line changes.

TransportGood forNotes
Learning TransportPractice and demosFiles on disk, no install, not for production
RabbitMQSelf-hosted, on-prem or cloudPopular, supports native pub-sub
Azure Service BusApps on AzureFully managed by Microsoft
Amazon SQS / SNSApps on AWSManaged queues and topics

Swapping is as small as this:

// From this (practice):
endpointConfig.UseTransport(new LearningTransport());
 
// To this (Azure Service Bus in production):
endpointConfig.UseTransport(new AzureServiceBusTransport(connectionString));

Because your handlers never mention the transport, you can start on the Learning Transport today and move to a real broker tomorrow with almost no code change. This is one of the biggest gifts NServiceBus gives a team.

A note on licensing and the .NET ecosystem

You may have heard that some popular .NET libraries changed their pricing. As of 2026, both MediatR and MassTransit moved to commercial licensing for newer versions. NServiceBus has always been a commercial product from Particular Software, but it offers a free Community Edition for small teams and non-production use, plus the free Learning Transport for practice. Before you ship NServiceBus in a paid product, read the current licensing page from Particular, because terms can change. For learning and small projects, you can get started at no cost.

This matters when you compare tools. NServiceBus is not just a message router. It bundles retries, pub-sub, long-running workflows (called sagas), and the outbox for safe message delivery, all in one well-supported package. For many teams, that maturity is worth the license.

A simple end-to-end picture

Let us put the whole flow together with three endpoints: a Web endpoint that takes orders, a Shipping endpoint that processes them, and Email plus Loyalty endpoints that react to the news.

The full journey of one order through several endpoints.

Read it from the top. The user posts an order. The Web endpoint sends a PlaceOrder command into the Shipping queue and answers the user right away. The Shipping handler processes the order, then publishes an OrderPlaced event. The Email and Loyalty endpoints each catch that event and do their own work. No endpoint waits on another. Each one is calm and independent.

Good habits to start with

A few simple rules will keep your NServiceBus app healthy from day one.

  • Name events in the past tense. OrderPlaced, not PlaceOrderEvent. It keeps the command-versus-event line clear.
  • Keep messages small. They travel through a queue. Send IDs and a few fields, not whole object graphs.
  • One responsibility per handler. A handler should do one clear job. If it grows, split it.
  • Make handlers idempotent. Because retries exist, a handler might run twice for the same message. Design it so running twice does no harm, for example by checking "did I already process this order ID?"
  • Turn on the Outbox when a handler both writes to a database and sends messages. It makes the two happen together safely, so you never save data without sending the message, or send a message without saving the data.

That last point connects to a wider pattern. NServiceBus has a built-in outbox feature that solves the dual-write problem for you, which is a common source of subtle bugs in messaging systems.

Quick recap

  • NServiceBus is a parcel counter for your .NET code. Parts of your app hand it messages, and it delivers them safely and reliably.
  • An endpoint is a running program with NServiceBus inside. It can send, receive, or both, and it finds your handlers automatically at startup.
  • A command says "do this" and has one handler; you use Send. An event says "this happened" (past tense) and can have many listeners; you use Publish.
  • A handler is a class implementing IHandleMessages<T>. The IMessageHandlerContext lets you send, publish, and reply.
  • Publish-subscribe lets you add new listeners without changing the publisher. Auto-subscribe wires it up for you.
  • When work fails, NServiceBus does immediate retries, then delayed retries, then moves the message to the error queue so it can be fixed and replayed.
  • The transport (Learning, RabbitMQ, Azure Service Bus, SQS) is just one config line. Your handlers never change when you swap it.
  • Start free with the Learning Transport and Community Edition, and check Particular's current licensing before shipping in a paid product.

References and further reading

Related Patterns