Your first message bus works beautifully in development. Then a consumer throws on a malformed payload at 2 a.m., the message disappears, and nobody can tell you whether it was ever delivered. That is the moment teams discover the difference between a library that sends messages and a framework that owns delivery guarantees. In .NET, four libraries occupy that space — and they disagree fundamentally about how much they should do for you.

MassTransit (7,812 stars), Wolverine (2,398), Rebus (2,675), and NServiceBus (2,169) all publish messages over RabbitMQ, Azure Service Bus, Amazon SQS, or Kafka. All four support retries, sagas, and an outbox. The differences show up in the operational details: how endpoints are configured, what happens to a poison message, how much ceremony a new developer must learn, and what you pay — in licensing and in cognitive load — once the system has forty consumers instead of four.

TL;DR — The 30-Second Verdict

Pick Rebus if you want the smallest possible surface area and full control. It is a lean service bus: one AddRebus call, handlers by convention, no opinionated application framework. Best for teams of 1-5 services who already understand their broker.

Pick MassTransit if you want the largest ecosystem and the most documented patterns. Its consumer/endpoint/saga model is the de facto standard in .NET, and the documentation covers almost every topology you will encounter. Heaviest abstraction layer of the four.

Pick Wolverine if you want MassTransit-style capability with a convention-over-configuration core — and you like the idea of your message handlers doubling as HTTP endpoints and scheduled jobs. Newest of the four, fastest-moving, smallest amount of boilerplate.

Pick NServiceBus if you are building a long-lived, large-scale system and want a vendor behind it. You get the most rigorous saga and persistence tooling, plus commercial support — and you pay for a license once you outgrow the free tier.

Side-by-Side Comparison

DimensionMassTransitWolverineRebusNServiceBus
GitHub stars7,8122,3982,6752,169
LicenseApache 2.0MITMITCommercial (free tier)
Last commit2026-10-212026-10-302026-09-292026-10-30
Primary abstractionIBus + consumersHandlers + cascading messagesHandlers + IBusIEndpointInstance + handlers
TransportsRabbitMQ, Azure SB, SQS, Kafka, SQL, in-memoryRabbitMQ, Azure SB, SQS, Kafka, PostgreSQL, SQL Server, local queuesRabbitMQ, Azure SB, SQS, Kafka, SQL, file systemRabbitMQ, Azure SB, SQS, MSMQ
Built-in outboxYes (EF Core, MongoDB, SQL)Yes (durable inbox/outbox)Yes (via Rebus.TransactionScopes/outbox package)Yes (SQL, NHibernate, EF)
Saga supportState machine + orchestrationYes, with persistenceYes (Saga base class)Yes (most mature)
Boilerplate per consumerMediumLowLowHigh
Learning curveMediumLow-mediumLowHigh
Best fitLarge .NET codebasesGreenfield servicesSmall/medium fleetsRegulated enterprise

Decision Matrix: Match the Bus to Your Situation

Your situationRecommendedWhy
3 services, one team, RabbitMQ already runningRebusOne registration call, no framework lock-in, trivial to reason about
15+ consumers, sagas, existing ASP.NET Core appMassTransitMature endpoint configuration, huge pattern library, easy hiring
Greenfield modular monolith that may split laterWolverineHandlers work in-process today and over a broker tomorrow without rewrites
Regulated workload needing vendor support and auditsNServiceBusCommercial backing, strict saga persistence, supported upgrade path
You need to move 50k messages/min on a budgetWolverine or MassTransitBoth batch and prefetch aggressively; Rebus needs more manual tuning
Team has zero messaging experienceRebus, then growThe smallest concept count — bus, handler, transport

MassTransit — The Ecosystem Default

MassTransit’s model is explicit: you register consumers, then bind them to receive endpoints. That explicitness is why it scales to large systems — every queue in your broker maps to a line you can point at in code.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
builder.Services.AddMassTransit(x =>
{
    x.AddConsumer<OrderPlacedConsumer>();

    x.AddEntityFrameworkOutbox<OrderDbContext>(o =>
    {
        o.UsePostgres();
        o.UseBusOutbox();
    });

    x.UsingRabbitMq((context, cfg) =>
    {
        cfg.Host("rabbitmq", "/", h =>
        {
            h.Username("mtuser");
            h.Password("mtpass");
        });

        cfg.ReceiveEndpoint("orders", e =>
        {
            e.PrefetchCount = 32;
            e.UseMessageRetry(r => r.Exponential(5,
                TimeSpan.FromSeconds(1),
                TimeSpan.FromSeconds(30),
                TimeSpan.FromSeconds(5)));
            e.ConfigureConsumer<OrderPlacedConsumer>(context);
        });
    });
});

The consumer itself is a class with a typed Consume method:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public class OrderPlacedConsumer : IConsumer<OrderPlaced>
{
    private readonly IPublishEndpoint _publisher;

    public OrderPlacedConsumer(IPublishEndpoint publisher) => _publisher = publisher;

    public async Task Consume(ConsumeContext<OrderPlaced> context)
    {
        await _publisher.Publish(new InventoryReserved(context.Message.OrderId));
    }
}

Where it wins: endpoint-level configuration is a first-class concept. Retry policies, prefetch, concurrency limits, and dead-letter behaviour are all per-queue. The state machine saga (MassTransitStateMachine<T>) is expressive enough that most teams never hand-write a saga.

Where it costs you: the abstraction stack is deep. When a message vanishes, you debug through IBus, IBusControl, receive endpoints, and the broker — four layers. New developers also need to internalize the difference between Send (queue-targeted) and Publish (fan-out), which is the single most common source of “why did only one service get it?” bugs.

Wolverine — Conventions Over Ceremony

Wolverine starts from a different premise: a message handler is just a method, and the framework infers everything else. The same handler can be invoked in-process, over RabbitMQ, or as an HTTP endpoint.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
builder.Host.UseWolverine(opts =>
{
    opts.UseRabbitMq(new Uri("rabbitmq://mtuser:mtpass@rabbitmq:5672"))
        .AutoProvision();

    opts.ListenToRabbitQueue("orders")
        .MaximumParallelMessages(16);

    opts.Policies.UseDurableInbox();
    opts.Policies.AutoApplyTransactions();
});

public static class OrderPlacedHandler
{
    public static InventoryReserved Handle(OrderPlaced message)
        => new InventoryReserved(message.OrderId);
}

That return value is the interesting part: Wolverine treats returned objects as cascading messages and routes them automatically. No publisher injection, no explicit Send call. It also generates the routing for you — AutoProvision() creates queues and bindings on startup, which removes an entire class of “the queue did not exist yet” incidents.

Where it wins: the lowest concept count of any full-featured bus. AutoApplyTransactions() and the durable inbox/outbox are opt-in one-liners rather than multi-part configuration. Startup provisioning means a fresh environment converges without manual broker setup.

Where it costs you: conventions are invisible. When routing goes wrong, the fix is often understanding an inferred rule rather than editing an explicit binding. The ecosystem is smaller (2,398 stars vs MassTransit’s 7,812), so fewer Stack Overflow answers exist for edge cases.

Rebus — The Lean Service Bus

Rebus does the minimum a service bus must do, and does it predictably. One registration, explicit routing, handlers found by interface.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
services.AddRebus(configure => configure
    .Transport(t => t.UseRabbitMq("amqp://mtuser:mtpass@rabbitmq", "orders"))
    .Routing(r => r.TypeBased()
        .Map<OrderPlaced>("orders")
        .Map<InventoryReserved>("inventory"))
    .Options(o =>
    {
        o.SetNumberOfWorkers(8);
        o.SetMaxParallelism(16);
        o.EnableDataBus();
    }));

public class OrderPlacedHandler : IHandleMessages<OrderPlaced>
{
    public async Task Handle(OrderPlaced message)
    {
        // handle it
    }
}

Where it wins: you can read the entire configuration of a service in twenty lines. Routing is explicit — TypeBased() mapping tells you exactly which queue receives which message, which makes production debugging dramatically faster than tracing inferred conventions. Rebus also has unusually good support for non-broker transports (SQL Server, file system), useful for air-gapped environments.

Where it costs you: fewer batteries. Retry, outbox, and saga support exist but require installing and wiring separate packages, and there is no state-machine DSL — sagas are hand-written classes. For a two-service system that is a feature; for forty consumers it becomes work you repeat.

NServiceBus — The Enterprise Option

NServiceBus is the oldest and most opinionated of the four. It assumes you want a message-driven architecture with strict persistence, and it enforces that shape.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
var endpointConfiguration = new EndpointConfiguration("Orders");

endpointConfiguration.UseTransport(new RabbitMQTransport(
    RoutingTopology.Conventional(QueueType.Quorum),
    "host=rabbitmq;username=nsb;password=nsbpass"));

endpointConfiguration.UsePersistence<SqlPersistence>();
endpointConfiguration.EnableInstallers();

var endpointInstance = await Endpoint.Start(endpointConfiguration);

Note QueueType.Quorum — NServiceBus defaults to durable, replicated quorum queues rather than classic queues. That single default tells you who the target customer is: teams that cannot afford to lose a message and will pay to avoid designing that guarantee themselves.

Where it wins: sagas, persistence, and upgrade tooling are the most rigorous here. EnableInstallers() provisions queues, tables, and subscriptions at startup. Commercial support means an escalation path when a broker-level bug eats your weekend.

Where it costs you: licensing. It is free for development and small deployments but requires a paid license at scale — a real budget line that the other three do not have. The API is also the most ceremonious: an endpoint is an object you start and stop yourself, which is awkward inside the generic host if you are used to AddMassTransit.

The Broker Underneath: Docker Compose You Can Actually Run

All four buses are only as reliable as the broker. This compose file gives you RabbitMQ 4.1 with the management UI and a healthcheck — enough to reproduce the retry and dead-letter behaviour described above:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
services:
  rabbitmq:
    image: rabbitmq:4.1-management
    ports:
      - "5672:5672"
      - "15672:15672"
    environment:
      RABBITMQ_DEFAULT_USER: mtuser
      RABBITMQ_DEFAULT_PASS: mtpass
    volumes:
      - rabbitmq-data:/var/lib/rabbitmq
    healthcheck:
      test: ["CMD", "rabbitmq-diagnostics", "-q", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5

volumes:
  rabbitmq-data:

Then verify a queue is actually consuming:

1
2
3
docker compose up -d
docker compose exec rabbitmq rabbitmqctl list_queues name messages consumers
# orders   0   4     <- 4 consumers attached, 0 backlog

Pitfalls That Bite Every Team Once

Shared contract assemblies are a trap at scale. All four buses encourage putting message types in a shared NuGet package. That works until two services need different versions of the same event. Prefer additive contracts — add properties, never rename or remove — and treat the contract package as a public API with its own versioning.

Prefetch is not a performance dial, it is a fairness dial. Setting PrefetchCount to 500 makes one consumer hoover up the queue; the other three sit idle. Start at 16-32 and raise it only after measuring handler latency.

None of these give you exactly-once delivery. They give you at-least-once plus an outbox. If a handler is not idempotent, a retry will double-charge a customer. Every consumer needs an idempotency key (usually the message ID) written in the same transaction as its side effect.

Poison messages must have an owner. Configure a dead-letter or error queue explicitly and alert on its depth. A silently growing orders_error queue is the most common failure in .NET messaging, and it fails quietly.

Do not do work in Task.Run inside a consumer. The bus will consider the message handled when the method returns. Background work gets lost on scale-in, and retries will not help because the message is already acked.

Quorum queues cost disk. Replicated queues triple storage and add latency. Use them for financial or state-transition messages, and classic queues for high-volume telemetry where losing a message is acceptable.

FAQ

Which .NET message bus is fastest to get running?

Rebus. A single AddRebus call with one transport and one routing rule gives you a working producer and consumer. MassTransit needs consumers plus endpoint configuration; Wolverine needs a host policy block; NServiceBus needs an endpoint object you start and stop.

Can I use these without RabbitMQ?

Yes, all four abstract the transport. MassTransit supports Azure Service Bus, Amazon SQS, Kafka, SQL, and in-memory. Wolverine adds PostgreSQL and SQL Server queues plus durable local queues. Rebus supports SQL Server, Azure Service Bus, Kafka, and a file-system transport. NServiceBus supports RabbitMQ, Azure Service Bus, SQS, and MSMQ. In-memory transports are for tests only — they hide ordering and retry behaviour.

Do I need sagas, or is a database transaction enough?

If a business process spans more than one service and can pause (waiting for payment, for stock, for a human), you need a saga or process manager. MassTransit’s state machine and NServiceBus’s saga persistence are the most capable. Rebus and Wolverine support sagas too, with less tooling around them. If everything fits in one transaction, skip sagas entirely — they are operational overhead.

How do I migrate from MassTransit to Wolverine?

Both use typed message classes and DI-injected handlers, so the migration is mechanical: replace IConsumer<T> classes with handler methods, replace AddMassTransit with UseWolverine, and convert ReceiveEndpoint configuration into ListenToRabbitQueue calls. Run both buses in parallel against separate queues during the cutover so neither side loses messages.

Which one handles 50,000 messages per minute?

Wolverine and MassTransit both reach that comfortably with batching and prefetch tuning on RabbitMQ, provided handlers do not block on synchronous I/O. Rebus can too, but expect to tune SetNumberOfWorkers and SetMaxParallelism manually. At that volume the broker, not the library, is usually the bottleneck — measure queue depth before rewriting anything.

Is the free tier of NServiceBus enough for production?

For small deployments it can be, but the license terms escalate with endpoints and environments, so budget for it before you commit. If a license is not in the budget, MassTransit (Apache 2.0), Wolverine (MIT), and Rebus (MIT) impose no such constraint.

For the surrounding architecture, see our ASP.NET Core vs ServiceStack vs Carter web framework comparison, the .NET dependency injection container shootout, and our Hangfire vs Quartz.NET vs Coravel job scheduling guide — scheduled jobs and message handlers solve overlapping problems and are often deployed together.


💰 想测试你的市场判断力?我用 Polymarket 做预测市场交易——这是全球最大的预测市场平台,从大选结果到技术监管时间线,什么都可以押注。和赌博不同,这是真正的信息市场:你懂的信息越多,胜率越高。我靠预测技术相关事件的走向已经赚了不少。用我的邀请链接注册:Polymarket.com