Skip to main content

Multiple endpoint hosting

This post is part of the NServiceBus 10 Improvements series. (See more)

Multiple cats representing messaging endpoints, all chasing envelopes that represent messages

Throughout the history of NServiceBus, there has been a fairly standard practice:

1 endpoint == 1 queue == 1 process

The endpoint owned everything within the process boundary and used assembly scanning to find and wire up all the things that make up that endpoint. While you could use “satellites”, you really couldn’t put more than one full endpoint inside a process boundary.

But… what if it didn’t have to be that way?

🔗Herding cats

When you’re used to working with a single application, deployment is easy. But deploying distributed systems can get complicated fast.

For simple systems with a few endpoints, it’s actually pretty great. Splitting functions into separate services actually gives you the flexibility you didn’t have before. Sure, you now have a handful of different things to deploy, but it’s actually nice that lots of times you only have to deploy one of them at any given time. The website stays up while a background service updates, and website users don’t even notice any downtime.

What happens when you’ve got a multi-tenant system, and each tenant requires a half dozen microservices, a set of background queues for longer-running tasks, and a couple of independent endpoints specifically for handling a process that, due to external factors, needs 1-at-a-time processing of specific messages to maintain message ordering requirements.

…and now you have 200 tenants. And each tenant is on a different customer tier, with different scale-out requirements.

Congratulations, even though you have the same number of logical endpoints 1 you’re now orchestrating the deployment of hundreds of processes.

Not what you signed up for?

🔗One process, many isolated endpoints

Instead of assigning one process to each endpoint, imagine a single .NET host running multiple fully isolated NServiceBus endpoints side by side.

One application host could run all the endpoints for a single tenant as described above. From the outside, existing tooling still sees distinct endpoints. Queues remain separate, monitoring still reflects individual endpoint behavior, and operational boundaries stay recognizable.

From an infrastructure perspective, the deployment becomes a single host.

This creates a practical middle ground between two extremes. Teams no longer need to choose only between “everything shared” and “everything separately hosted.” They gain another option that preserves endpoint boundaries while reducing host sprawl.

But until now, colocating endpoints inside a single host was nearly impossible. The hosting model assumed one endpoint owned the process. Assembly scanning found every single thing and wired up all of it. Meanwhile, a single dependency injection context did not allow different services to be resolved for independent endpoints.

Assembly scanning was the culprit here, so now we had two major reasons to change. If properly redesigned, we could support multiple endpoints and move toward a future where we support assembly trimming and ahead-of-time compilation.

🔗Host-level isolation

These two goals were the driving force of the changes in NServiceBus version 10.0, and now in version 10.2 we now support multiple endpoints with a new hosting model, and have simplified the NServiceBus hosting story with a single model based on the .NET Generic Host.

Hosting an NServiceBus endpoint is now similar to registering any other service in the service collection - just call AddNServiceBusEndpoint:

// One host, one endpoint
services.AddNServiceBusEndpoint(endpointConfiguration);

But you can also host multiple endpoints. When hosting more than one endpoint, it’s important to keep them independent by providing an endpoint identifier to use as a service key so that the endpoint can tell between the services meant for it to use and the services meant for other endpoints. Most of the time, the service key can just be the endpoint name:

services.AddNServiceBusEndpoint(salesCfg, salesEndpointCfg.EndpointName);
services.AddNServiceBusEndpoint(billingCfg, salesEndpointCfg.EndpointName);

If you need to register independent services for an endpoint, use service collection methods like AddKeyedSingleton along with the service key you used for the endpoint:

builder.Services.AddKeyedSingleton<DatabaseService>(salesEndpointCfg.EndpointName, salesDb);

It’s only when you get to more complex scenarios (like the multi-tenant example above) that you would need to specify custom endpoint identifiers that are different than the endpoint name:

// One host, many endpoints
services.AddNServiceBusEndpoint(tenantAConfiguration, "tenant-a-main");
services.AddNServiceBusEndpoint(tenantAP1Configuration, "tenant-a-p1");

Each endpoint can maintain its own persistence configuration, database connections, recoverability settings, metrics, heartbeats, dependency registrations, and runtime pipeline. This provides real isolation rather than a shared-state compromise.

🔗Register only what belongs

Multi-endpoint hosting only works if each endpoint knows what it’s responsible for. Otherwise, we have the same problem as we had with assembly scanning.

But we didn’t want to make it a huge pain to register your handlers and sagas, and we also didn’t want to completely remove assembly scanning for all current single-endpoint deployments where it’s working well.

So assembly scanning is still turned on by default. That means that if you want to make use of multiple endpoints in your host, your EndpointConfiguration has to be configured to turn assembly scanning off:

salesEndpointConfig.AssemblyScanner().Disable = true;

Then you register the handlers and sagas. You can do that individually, if you want…

salesEndpointConfig.AddHandler<PlaceOrderHandler>();
salesEndpointConfig.AddSaga<BuyersRemorsePolicy>();

But it’s more efficient and succinct 2 to register multiple handlers using source-generated grouped registration methods, which give you the same convenience as assembly scanning, without using assembly scanning:

salesEndpointConfig.Handlers.SalesEndpoint.AddAll();

Making this code generation goodness work requires applying the [Handler] attribute to your handlers and the [Saga] attribute to each saga3, but we also created a Roslyn analyzer and code fix to do this for you.

Just add this to your .editorconfig file, and then forgetting the important attribute becomes a build error, so you can’t forget:

# Ensure all handlers are decorated with [Handler] to enable source generation
dotnet_diagnostic.NSB0022.severity = error
# Ensure all sagas are decorated with [Saga] to enable source generation
dotnet_diagnostic.NSB0025.severity = error

There is no accidental cross-registration, no scanning surprises, and less startup overhead. Each endpoint owns exactly what belongs there. For multi-endpoint hosting, that clarity matters. Isolation is stronger when the configuration is explicit.

This model also aligns naturally with architectural approaches such as Vertical Slice Architecture, where features are organized around capabilities rather than technical layers. Endpoints can host only the handlers, sagas, and dependencies that belong to a given slice, reinforcing both runtime and code-level boundaries.

🔗It’s all about boundaries

Don’t be fooled into thinking this multiple endpoints feature is all about “How much stuff can we cram in one bucket?” That would miss the point: it’s really about providing deployment flexibility to define system boundaries correctly.

Gregor Hohpe has highlighted how often tenancy and ownership are overlooked in platform design, especially when many teams share the same platform. The AWS Well-Architected SaaS Lens describes the same design space with silo, pool, and bridge models:

  • The silo model separates tenants, allowing complete ownership of allocated resources, with the risk that some of those resources are wasted. This is where the previous one-endpoint-per-process practice lives.
  • The pool model allows tenants to share all resources, allowing scale to ebb and flow between needs, but erasing ownership. Taken to an extreme, this would be like hosting all system endpoints together in a process.
  • The bridge model describes the pragmatic middle between the two.

Allowing multiple endpoints to coexist in a process enables the bridge model - but you can still isolate an endpoint in separate processes when you need to.

The design problem behind most distributed systems is not just endpoint density per process or the number of separate deployments, but how tenancy and ownership interact and where to define and enforce your boundaries.

So, now you have a lot more flexibility in defining where those boundaries should live, without a hard one-endpoint-per-process rule artificially limiting your options.

🔗Separation guaranteed

But even when many endpoints run in a single process, failed messages still fail per endpoint, retries still return to the correct endpoint, metrics remain endpoint-oriented, heartbeats remain endpoint-specific, custom checks stay isolated, and log scopes automatically include the endpoint name, so filtering stays straightforward. Existing monitoring tools continue to work as teams expect.

This is not simply process consolidation. It is a new deployment model that preserves the operational characteristics people rely on when they choose messaging in the first place.

🔗Most systems should still stay simple

It is worth stating clearly that many 4 systems should continue using one host, one container, and one endpoint. That model is clean, understandable, and operationally strong. It works well in many situations, and we took great care not to mess it up.

Multi-endpoint hosting is not about replacing the default. It is about removing an artificial limitation for systems with more complex needs, such as high tenant density, large numbers of similar small workloads, partition-heavy throughput models, or cost pressure created by host sprawl.

Good architecture is not about maximizing cleverness. It is about putting the right amounts of the right kinds of complexity specifically where they’re needed.

🔗What comes next

Multi-endpoint hosting offers significant flexibility for distributed system builders. Teams can explore dynamic tenant workers, partitioned high-throughput processors, dedicated co-located infrastructure endpoints, and new platform hosting models that were previously awkward or expensive to build.

The messaging principles remain the same, but teams gain more freedom in how they apply them.

Whether you’re hosting a single endpoint or multiple endpoints within a process, check out the new Hosting with Microsoft.Extensions.Hosting documentation to get started, where you’ll also find additional information on how to add keyed services to your IServiceCollection that are unique for each endpoint.

If you want more of a demo of what is possible with multiple endpoint hosting, similar to the multi-tenant scenario described at the beginning of this post, check out the original proof-of-concept which goes much further into routing, partitioning, and endpoint isolation for dynamic tenants loaded from configuration. Within that repository, you can find the main endpoint registration code in NServiceBusServiceCollectionExtensions.cs.

Share on Twitter

About the author

Daniel Marbach

Daniel Marbach is a core engineer at Particular Software who believes there's a difference between sharing endpoints and sharing toothbrushes.

More from the NServiceBus 10 Improvements series:

  1. …because there are a low number of endpoints that happen to be scaled out to multiple tenants, using the same code but deployed separately due to different configuration and scalability needs.

  2. …and dare I say, more fun?

  3. This is because source generators do not perform well when driven off implemented interfaces, so Microsoft recommends you Never Do That®. Scans using an attribute are the fastest and most optimized code path, which is important because a source generator can technically run every time you press a key in your IDE.

  4. Or even most?

Don't miss a thing. Sign up today and we'll send you an email when new posts come out.
Thank you for subscribing. We'll be in touch soon.
 
We collect and use this information in accordance with our privacy policy.