Skip to content

Graceful Shutdown ​

What happens to in-flight messages when a Modulus messaging host stops, and how to configure orchestrators around it.

Stop Ordering ​

.NET stops hosted services in reverse registration order. AddModulusMessaging registers the consumer host before the outbox processor, so shutdown proceeds:

  1. OutboxProcessor stops first — the dispatch loop observes cancellation (whether idle in its signal-or-poll wait or between batches) and stops fetching new batches. A dispatch pass that is mid-flight finishes its current message; unpublished rows simply stay in the outbox table and are dispatched after the next start. Nothing is lost: the outbox is durable by design.
  2. TransportConsumerHost stops second — it calls the transport's StopConsumingAsync, which cancels the broker subscription and drains in-flight handler work.
  3. The transport disposes last — connections close after consumers have stopped.

In-Flight Semantics Per Transport ​

TransportIn-flight message on shutdown
RabbitMQUnacknowledged deliveries return to the queue when the connection closes and are redelivered after restart. The inbox skips handlers that already completed; a handler interrupted mid-execution leaves a reservation that goes stale and is re-executed on redelivery.
Azure Service BusThe message lock expires (processors stop renewing on stop) and the message is redelivered to the subscription. Same inbox semantics as above.
In-memoryBuffered messages are drained during StopConsumingAsync within the shutdown window; anything beyond it is dropped (in-process transport, no durability — pair with the outbox for anything that matters).

Net effect with outbox + inbox + a broker transport: shutdown at any point is at-least-once with per-handler idempotency — no lost messages, no double-executed handlers.

Shutdown Window ​

The default host shutdown timeout is 30 seconds (HostOptions.ShutdownTimeout). Handlers that can run long should stay comfortably inside it; the in-process retry backoff (ConsumerRetry) also counts against the window, so a message stuck in a retry loop during shutdown will be cut off and redelivered later — which is fine, and exactly what the reservation stale-takeover handles.

csharp
builder.Services.Configure<HostOptions>(o => o.ShutdownTimeout = TimeSpan.FromSeconds(60));

Kubernetes Guidance ​

  • Set terminationGracePeriodSeconds above the host's ShutdownTimeout (e.g. 45s over a 30s timeout) so the kubelet doesn't SIGKILL mid-drain.
  • A SIGKILL (grace period exceeded, OOM) is equivalent to a crash: unacked broker messages redeliver, inbox reservations go stale and are taken over, unpublished outbox rows dispatch after restart. The system converges; you only lose the time.
  • Use the scaffolded /readyz endpoint (with the messaging health checks) as the readiness probe so traffic stops before shutdown begins in rolling updates.

See Also ​

Released under the MIT License.