Control Bus for .NET routes: stop, restart and react to route events at runtime
A running integration is not a static thing. A deploy needs to drain a queue and stop a route before the new build takes over. A downstream service falls over, and the route hammering it should back off instead of piling up retries. An operator needs to suspend one branch of the flow without restarting the whole process. Something fails at 3am, and the on-call dashboard should light up on its own, not because someone was tailing logs.
Apache Camel solved the operational half of this a long time ago with the Control Bus pattern: you manage routes by sending a message to a special endpoint, the same way you move any other message. redb.Route brings that pattern to .NET, with the Camel command set, and adds one thing Camel does not have: a consumer that turns route lifecycle events back into messages you can route.
So there are two directions here. Outbound: tell a route to start, stop, suspend, resume or restart. Inbound: subscribe to what routes are doing and react. Both are ordinary steps of a pipeline. Let us look at the code.
Control Bus in one minute
The component is registered out of the box, there is no package to add. Address it by URI or by the fluent DSL.
// Stop a route by id
.To("controlbus:route?routeId=orders&action=stop")
// Same thing, fluent
.ControlBus(ControlBusAction.Stop, "orders")
That is a producer step. When a message reaches it, the named route is stopped. Everything else is variations on the action and one consumer for events.
Manage a route by sending a message
The action is the verb. The full set mirrors Camel:
| Action | Effect |
|---|---|
Start |
Start the route. |
Stop |
Stop the route (consumer removed, the route stays registered). |
Suspend |
Suspend the route. |
Resume |
Resume a stopped or suspended route. |
Restart |
Stop, then start after restartDelay (default 1000 ms). |
Status |
Report the route's status. |
Stats |
Report the route's statistics. |
Fail |
Stop the route and mark it failed. |
In the fluent form the action is an enum; as a URI it is the action query parameter.
using redb.Route.ControlBus;
.ControlBus(ControlBusAction.Suspend, "payments")
.ControlBus(ControlBusAction.Restart, "orders", async: true) // fire-and-forget
// URI equivalents
.To("controlbus:route?routeId=payments&action=suspend")
.To("controlbus:route?routeId=orders&action=restart&async=true")
Because it is a normal route step, the control action can be the tail of any pipeline. A timer that suspends a batch route outside business hours. A webhook that restarts a route on a config change. A choice branch that stops a consumer when a poison message is seen. You are not calling a management API from the outside; you are routing a message, with all the same retries, error handling and observability as the rest of the flow.
A route can manage itself
Pass current as the route id and the action targets the route that is sending the message. This is how a route reacts to its own condition.
From("kafka://orders")
.Process(CheckHealth)
.Choice()
.When(e => e.In.GetHeader<bool>("downstreamDown"))
.ControlBus(ControlBusAction.Suspend, "current") // back off, stop consuming
.End();
A route that detects its downstream is unhealthy suspends itself instead of spinning through failures. Something external, a timer or an operator message, resumes it later.
The part Camel does not have: react to route events
This is the direction that is usually missing. In Camel the Control Bus is producer-only: you send commands, you do not subscribe to what happens. redb.Route adds controlbus:notify, a consumer that emits route and context lifecycle events as messages. You put it on the From side and route the events anywhere.
From("controlbus:notify")
.Process(e =>
{
var evt = e.In.GetHeader<string>(ControlBusHeaders.Event); // RouteStarted, RouteStopped, RouteErrored, ...
var route = e.In.GetHeader<string>(ControlBusHeaders.RouteId);
var when = e.In.GetHeader<DateTimeOffset>(ControlBusHeaders.Timestamp);
})
.To("kafka://route-events");
The events cover the lifecycle of routes and of the context itself: RouteStarted, RouteStopped, RouteSuspending, RouteErrored, ContextStarting, ContextStarted, ContextStopping, ContextStopped, ExchangeTimedOut. Each message carries the details as headers: the event name, the affected route id, a timestamp, and where relevant the error, the exchange id and the elapsed time.
Filter to what you care about with the events and routeId parameters:
// Only failures and stops, only for the orders route
From("controlbus:notify?events=RouteErrored,RouteStopped&routeId=orders")
.To("slack://alerts");
Now the interesting part is that both directions compose. Events on the way in, commands on the way out, in one route: this is a self-healing loop with no external control plane.
From("controlbus:notify?events=RouteErrored")
.Process(e => e.In.SetHeader("failed", e.In.GetHeader<string>(ControlBusHeaders.RouteId)))
.Delay(TimeSpan.FromSeconds(30))
.ControlBus(ControlBusAction.Restart, "current"); // or the captured route id
An errored route emits an event, a route consumes it, waits, and restarts the offender. The supervision logic is a pipeline, visible and testable like any other, not a hardcoded policy buried in the engine.
What this replaces
Without a Control Bus, runtime route management is a pile of one-off plumbing. A custom admin controller that reaches into the engine to stop a route. A BackgroundService that polls a flag table to know when to pause. A logging appender wired to an alerting SDK so someone hears about a failure. Each is a separate mechanism, with its own lifecycle, its own tests, its own way of being wrong.
The Control Bus turns all of that into routing. Management commands are messages To an endpoint. Lifecycle events are messages From an endpoint. They go through the same DSL, the same error handling, the same OpenTelemetry traces as your business flows, and they land in the same place your team already looks.
- Zero-downtime deploys: suspend and drain a route on a signal, flip, resume.
- Backpressure and circuit-breaking at the route level: a route suspends itself when its downstream is unhealthy.
- Operational events as data: pipe
RouteErroredandRouteStoppedinto Kafka, Slack, a metrics sink, an incident tool, an audit log. - Self-healing: notify in, restart out, in one small route.
Camel parity, and beyond
If you come from Camel, the command side is familiar: controlbus:route with routeId and action, plus controlbus:language for expression-based control, producer-only, the standard verbs. redb.Route matches that command set. The controlbus:notify consumer is the addition: Camel gives you EventNotifier as an SPI you implement in Java and register; redb.Route gives you the same information as a first-class endpoint you route from, no interface to implement, no wiring, just From("controlbus:notify").
FAQ
Is it a separate package? No. Control Bus is part of core redb.Route and is registered out of the box. Nothing to install.
Does stopping a route remove it? No. Stop and Suspend remove the consumer but keep the route registered, so Resume or Start brings it back. Fail stops it and marks it errored.
Can a route control another route, not just itself? Yes. Pass the target route id instead of current.
What events are available? Route lifecycle (RouteStarted, RouteStopped, RouteSuspending, RouteErrored), context lifecycle (ContextStarting through ContextStopped), and ExchangeTimedOut. Filter with events= and routeId=.
Is the command synchronous? By default yes; pass async=true (or async: true in the DSL) for fire-and-forget, which also avoids a route trying to stop itself synchronously mid-exchange.
Where it lives
Control Bus ships inside redb.Route on NuGet; the DSL and the controlbus:notify event set are in the documentation. It is one of the 30+ EIP patterns in the framework, and like the rest it is a step of a route: the same From → … → To, the same observability. The difference is that the message it carries is a route telling you what it just did, or you telling a route what to do next.
If this was useful — a ⭐ on GitHub helps others find it.
More of my writing: redbase.app/articles, and on dev.to.