Error Handling
It's an imperfect world and almost inevitable that your Wolverine message handlers will occasionally throw exceptions as message handling fails. Maybe because a piece of infrastructure is down, maybe you get transient network issues, or maybe a database is overloaded.
Wolverine comes with two flavors of error handling (so far). First, you can define error handling policies on message failures with fine-grained control over how various exceptions on different message. In addition, Wolverine supports a per-endpoint circuit breaker approach that will temporarily pause message processing on a single listening endpoint in the case of a high rate of failures at that endpoint.
Error Handling Rules
WARNING
When using IMessageBus.InvokeAsync() to execute a message inline, only the "Retry" and "Retry With Cooldown" error policies are applied to the execution automatically. In other words, Wolverine will attempt to use retries inside the call to InvokeAsync() as configured. Custom actions can be explicitly enabled for execution inside of InvokeAsync() as shown in a section below.
Error handling rules in Wolverine are defined by three things:
- The scope of the rule. Really just per message type or global at this point.
- Exception matching
- One or more actions (retry the message? discard it? move it to an error queue?)
What to do on an error?
| Action | Description |
|---|---|
| Retry | Immediately retry the message inline without any pause |
| Retry with Cooldown | Wait a short amount of time, then retry the message inline |
| Requeue | Put the message at the back of the line for the receiving endpoint |
| Schedule Retry | Schedule the message to be retried at a certain time |
| Discard | Log, but otherwise discard the message and do not attempt to execute again |
| Move to Error Queue | Move the message to a dedicated dead letter queue and do not attempt to execute again |
| Pause the Listener | Stop all message processing on the current listener for a set duration of time |
While we think the options above will suffice for most scenarios, it's possible to create your own action through Wolverine's IContinuation interface.
So what to do in any particular scenario? Here's some initial guidance:
- If the exception is a common, transient error like timeout conditions or database connectivity errors, build in a limited set of retries and potentially use exponential backoff to avoid overloading your system (sample of this below)
- If the exception tells you that the message is invalid or could never be processed, discard the message
- If an exception happens on multiple attempts, move the message to a "dead letter queue" where it might be possible to replay at some later time
- If an exception tells you than the system or part of the system itself is completely down, you may opt to pause the message listening altogether
Moving Messages to an Error Queue
TIP
The actual mechanics of the error or "dead letter queue" vary between messaging transport
By default, a message will be moved to an error queue when it exhausts all its configured retry/requeue slots dependent upon the exception filter. You can, however explicitly short circuit the retries and immediately send a message to the error queue like so:
using var host = await Host.CreateDefaultBuilder()
.UseWolverine(opts =>
{
// Don't retry, immediately send to the error queue
opts.OnException<TimeoutException>().MoveToErrorQueue();
}).StartAsync();Discarding Messages
If you can detect that an exception means that the message is invalid in your system and could never be processed, just tell Wolverine to discard it:
using var host = await Host.CreateDefaultBuilder()
.UseWolverine(opts =>
{
// Bad message, get this thing out of here!
opts.OnException<InvalidMessageYouWillNeverBeAbleToProcessException>()
.Discard();
}).StartAsync();You have to explicitly discard a message or it will eventually be sent to a dead letter queue when the message has exhausted its configured retries or requeues.
Exponential Backoff
TIP
This error handling strategy is effective for slowing down or throttling processing to give a distressed subsystem a chance to recover
Exponential backoff error handling is easy with either the RetryWithCooldown() syntax shown below:
using var host = await Host.CreateDefaultBuilder()
.UseWolverine(opts =>
{
// Retry the message again, but wait for the specified time
// The message will be dead lettered if it exhausts the delay
// attempts
opts
.OnException<SqlException>()
.RetryWithCooldown(50.Milliseconds(), 100.Milliseconds(), 250.Milliseconds());
}).StartAsync();Or through attributes on a single message:
[RetryNow(typeof(SqlException), 50, 100, 250)]
public class MessageWithBackoff
{
// whatever members
}Jitter
When many nodes retry at the same fixed delay after a shared downstream failure, they produce a "thundering herd" that pounds the recovering dependency in lockstep. Wolverine supports additive jitter on the three delay-based error policies: RetryWithCooldown, ScheduleRetry / ScheduleRetryIndefinitely, and PauseThenRequeue.
Invariant: jitter only extends the configured delay, never shortens it. The configured values remain the lower bound.
Three strategies are available. They are mutually exclusive per error rule.
Jitter is applied once per error rule — all slots in the rule share the same strategy, including those added via .Then. Attempting to call a second WithXxxJitter() method on the same rule (even after .Then) throws InvalidOperationException.
WithFullJitter
Effective delay ∈ [d, 2·d].
opts.OnException<DownstreamUnavailableException>()
.RetryWithCooldown(50.Milliseconds(), 100.Milliseconds(), 250.Milliseconds())
.WithFullJitter();WithBoundedJitter
Effective delay ∈ [d, d × (1 + percent)]. Useful when you deliberately picked the cooldown values and want to keep the spread narrow.
opts.OnException<DownstreamUnavailableException>()
.ScheduleRetry(1.Seconds(), 5.Seconds(), 30.Seconds())
.WithBoundedJitter(0.25); // +0% to +25%percent must be greater than zero; there is no upper bound.
WithExponentialJitter
Effective delay ∈ [d, d × (1 + 2·attempt)]. The spread widens with every attempt, so persistent failures fan out more than transient ones. This is an attempt-scaled, stateless variant of the "decorrelated jitter" pattern — it deliberately avoids persisting the previous actual delay per envelope.
opts.OnException<DownstreamUnavailableException>()
.PauseThenRequeue(5.Seconds())
.WithExponentialJitter();Pausing Listening on Error Conditions
TIP
This feature exists in Wolverine because of the exact scenario described as an example in this section. Wish we'd had Wolverine then...
A common usage of asynchronous messaging frameworks is to make calls to an external API as a discrete step within a discrete message handler to isolate the calls to that external API from the rest of your application and put those calls into its own, isolated retry loop in the case of failures. Great! But what if something happens to that external API such that it's completely unable to accept any requests without manual intervention? You don't want to keep retrying messages that will just fail and eventually land in a dead letter queue where they can't be easily retried without manual intervention.
Instead, let's just tell Wolverine to immediately pause all message processing in the incoming message listener when a certain exception is detected like so:
using var host = await Host.CreateDefaultBuilder()
.UseWolverine(opts =>
{
// The failing message is requeued for later processing, then
// the specific listener is paused for 10 minutes
opts.OnException<SystemIsCompletelyUnusableException>()
.Requeue().AndPauseProcessing(10.Minutes());
}).StartAsync();Scoping
TIP
To be clear, the error rules are "fall through," meaning that the rules are evaluated in order.
In order of precedence, exception handling rules can be defined at either the specific message type or globally. As a third possibility, you can use a chain policy to specify exception handling rules with any kind of user defined logic -- usually against a subset of message types.
TIP
The Wolverine team recommends using one style (attributes or fluent interface) or another, but not to mix and match styles too much within the same application so as to make reasoning about the error handling too difficult.
First off, you can define error handling rules for a specific message type by placing attributes on either the handler method or the message type itself as shown below:
public class AttributeUsingHandler
{
[ScheduleRetry(typeof(IOException), 5)]
[RetryNow(typeof(SqlException), 50, 100, 250)]
[RequeueOn(typeof(InvalidOperationException))]
[MoveToErrorQueueOn(typeof(DivideByZeroException))]
[MaximumAttempts(2)]
public void Handle(InvoiceCreated created)
{
// handle the invoice created message
}
}You can also use the fluent interface approach on a specific message type if you put a method with the signature public static void Configure(HandlerChain chain) on the handler class itself as in this sample:
public class MyErrorCausingHandler
{
// This method signature is meaningful
public static void Configure(HandlerChain chain)
{
// Requeue on IOException for a maximum
// of 3 attempts
chain.OnException<IOException>()
.Requeue();
}
public void Handle(InvoiceCreated created)
{
// handle the invoice created message
}
public void Handle(InvoiceApproved approved)
{
// handle the invoice approved message
}
}To specify global error handling rules, use the fluent interface directly on WolverineOptions.Handlers as shown below:
using var host = await Host.CreateDefaultBuilder()
.UseWolverine(opts =>
{
opts.Policies.OnException<TimeoutException>().ScheduleRetry(5.Seconds());
opts.Policies.OnException<SecurityException>().MoveToErrorQueue();
// You can also apply an additional filter on the
// exception type for finer grained policies
opts.Policies
.OnException<SocketException>(ex => ex.Message.Contains("not responding"))
.ScheduleRetry(5.Seconds());
}).StartAsync();TODO -- link to chain policies, after that exists:)
Lastly, you can use chain policies to add error handling policies to a selected subset of message handlers. First, here's a sample policy that applies an error handling policy based on SqlException errors for all message types from a certain namespace:
// This error policy will apply to all message types in the namespace
// 'MyApp.Messages', and add a "requeue on SqlException" to all of these
// message handlers
public class ErrorHandlingPolicy : IHandlerPolicy
{
public void Apply(IReadOnlyList<HandlerChain> chains, GenerationRules rules, IServiceContainer container)
{
var matchingChains = chains
.Where(x => x.MessageType.IsInNamespace("MyApp.Messages"));
foreach (var chain in matchingChains) chain.OnException<SqlException>().Requeue(2);
}
}Exception Filtering
TIP
While many of the examples in this page have shown simple policies based on the type SqlException, in real life you would probably want to filter on specific error codes to fine tune your error handling for SQL failures that are transient versus failures that imply the message could never be processed.
The attributes are limited to exception type, but the fluent interface has quite a few options to filter exception further with additional filters, inner exception tests, and compound filters:
sample_filtering_by_exception_type
Custom Actions
TIP
For the sake of granular error handling, it's recommended that your custom error handler code limit itself to publishing additional messages rather than trying to do work inline
Wolverine will enable you to create custom exception handling actions as additional steps to take during message failures. As an example, let's say that when your system is sent a ShipOrder message you'd like to send the original sending service a corresponding ShippingFailed message when that ShipOrder message fails during processing.
The following code shows how to do this with an inline function:
theReceiver = await Host.CreateDefaultBuilder()
.UseWolverine(opts =>
{
opts.ListenAtPort(receiverPort);
opts.ServiceName = "Receiver";
opts.Policies.OnException<ShippingFailedException>()
.Discard().And(async (_, context, _) =>
{
if (context.Envelope?.Message is ShipOrder cmd)
{
await context.RespondToSenderAsync(new ShippingFailed(cmd.OrderId));
}
});
}).StartAsync();Optionally, you can implement a new type to handle this same custom logic by subclassing the Wolverine.ErrorHandling.UserDefinedContinuation type like so:
public class ShippingOrderFailurePolicy : UserDefinedContinuation
{
public ShippingOrderFailurePolicy() : base(
$"Send a {nameof(ShippingFailed)} back to the sender on shipping order failures")
{
}
public override async ValueTask ExecuteAsync(IEnvelopeLifecycle lifecycle, IWolverineRuntime runtime,
DateTimeOffset now, Activity? activity)
{
if (lifecycle.Envelope?.Message is ShipOrder cmd)
{
await lifecycle
.RespondToSenderAsync(new ShippingFailed(cmd.OrderId));
}
}
}and register that secondary action like this:
theReceiver = await Host.CreateDefaultBuilder()
.UseWolverine(opts =>
{
opts.ListenAtPort(receiverPort);
opts.ServiceName = "Receiver";
opts.Policies.OnException<ShippingFailedException>()
.Discard().And<ShippingOrderFailurePolicy>();
}).StartAsync();Circuit Breaker
TIP
At this point, the circuit breaker mechanics need to be applied on an endpoint by endpoint basis
Wolverine also supports a circuit breaker strategy for handling errors. The purpose of a circuit breaker is to pause message handling for a single endpoint if there are a significant percentage of message failures in order to allow the system to catch up and possibly allow for a distressed subsystem to recover and stabilize.
The usage of the Wolverine circuit breaker is shown below:
using var host = await Host.CreateDefaultBuilder()
.UseWolverine(opts =>
{
opts.Policies.OnException<InvalidOperationException>()
.Discard();
opts.ListenToRabbitQueue("incoming")
.CircuitBreaker(cb =>
{
// Minimum number of messages encountered within the tracking period
// before the circuit breaker will be evaluated
cb.MinimumThreshold = 10;
// The time to pause the message processing before trying to restart
cb.PauseTime = 1.Minutes();
// The tracking period for the evaluation. Statistics tracking
cb.TrackingPeriod = 5.Minutes();
// If the failure percentage is higher than this number, trip
// the circuit and stop processing
cb.FailurePercentageThreshold = 10;
// Optional allow list
cb.Include<NpgsqlException>(e => e.Message.Contains("Failure"));
cb.Include<SocketException>();
// Optional ignore list
cb.Exclude<InvalidOperationException>();
});
}).StartAsync();Note that the exception includes and excludes are optional. If there are no explicit Include() calls, the circuit breaker will assume that every exception should be considered a failure. Likewise, if there are no Exclude() calls, the circuit breaker will not throw out any exceptions. Also note that it probably makes no sense to define both Include() and Exclude() rules.
Custom Actions for InvokeAsync() 3.13
INFO
This usage was built for a JasperFx Software customer who is using Wolverine by calling IMessageBus.InvokeAsync() directly underneath Hot Chocolate mutations. In their case, if the mutation action failed more than X number of times, they wanted to send a different message that would try to jumpstart the long running workflow that is somehow stalled.
This is maybe a little specialized, but let's say you have a reason for calling IMessageBus.InvokeAsync() inline, and that you want to carry out some kind of custom action if the message handler exceeds a certain number of retries (the only error handling action that applies automatically to InvokeAsync()). You can now opt custom actions into applying to exceptions thrown by your message handlers during a call to InvokeAsync() by specifying an InvokeResult value of Stop or TryAgain to a custom action. Here's a sample that uses a CompensatingAction() helper method for raising other messages on failures:
public record ApproveInvoice(string InvoiceId);
public record RequireIntervention(string InvoiceId);
public static class InvoiceHandler
{
public static void Configure(HandlerChain chain)
{
chain.OnAnyException().RetryTimes(3)
.Then
.CompensatingAction<ApproveInvoice>((message, ex, bus) => bus.PublishAsync(new RequireIntervention(message.InvoiceId)),
// By specifying a value here for InvokeResult, I'm making
// this action apply to failures inside of IMessageBus.InvokeAsync()
InvokeResult.Stop);
// This is just a long hand way of doing the same thing as CompensatingAction
// .CustomAction(async (runtime, lifecycle, _) =>
// {
// if (lifecycle.Envelope.Message is ApproveInvoice message)
// {
// var bus = new MessageBus(runtime);
// await bus.PublishAsync(new RequireIntervention(message.InvoiceId));
// }
//
// }, "Send a compensating action", InvokeResult.Stop);
}
public static int SucceedOnAttempt = 0;
public static void Handle(ApproveInvoice invoice, Envelope envelope)
{
if (envelope.Attempts >= SucceedOnAttempt) return;
throw new Exception();
}
public static void Handle(RequireIntervention message)
{
Debug.WriteLine($"Got: {message}");
}
}Running custom actions indefinitely
In some scenarios you want your custom action to control the retry lifecycle across multiple attempts (e.g., reschedule with a delay until some external condition is met), instead of Wolverine moving the message to the error queue after the first attempt. For that, use CustomActionIndefinitely(...).
CustomActionIndefinitely keeps invoking your custom action on subsequent attempts until your code explicitly stops the process. Inside the delegate you can for example:
- Reschedule the message (e.g., with backoff, or by some dynamic values based on exception's payload....) via
lifecycle.ReScheduleAsync(...) - Requeue if appropriate
- Or stop further processing by calling
lifecycle.CompleteAsync()(optionally after logging or publishing a compensating message)
Example:
using var host = await Host.CreateDefaultBuilder()
.UseWolverine(opts =>
{
opts.Policies
.OnException<SpecialException>()
.CustomActionIndefinitely(async (runtime, lifecycle, ex) =>
{
// Stop after 10 attempts
if (lifecycle.Envelope.Attempts >= 10)
{
// Decide to stop trying; you could also move to an error queue
await lifecycle.CompleteAsync();
return;
}
// Keep trying later with a delay
await lifecycle.ReScheduleAsync(DateTimeOffset.UtcNow.AddSeconds(15));
}, "Handle SpecialException with conditional reschedule/stop");
}).StartAsync();Note that custom actions would always be applied to exceptions thrown in asynchronous message handling.
Fault Events
Wolverine ships an opt-in mechanism that auto-publishes a strongly-typed Fault<T> envelope whenever a handler for T terminally fails. Use it when distributed consumers want to react to failures programmatically, or when a typed projection of "what failed and why" is more useful than inspecting a generic dead-letter queue.
Fault events sit after your retry / requeue / DLQ rules above — they fire when a message has reached a terminal state per those rules, not before.
Quickstart
Opt-in globally:
opts.PublishFaultEvents();Subscribe with a normal Wolverine handler — no special attribute, no opt-in registration:
public static class OrderPlacedFaultHandler
{
public static void Handle(Fault<OrderPlaced> fault) =>
Console.WriteLine($"Order {fault.Message.Id} failed: {fault.Exception.Message}");
}Whenever OrderPlacedHandler fails terminally (retries exhausted, moved to error queue, or — if opted in — discarded), Wolverine publishes a Fault<OrderPlaced> envelope through the global routing graph.
Anatomy of Fault<T>
public record Fault<T>(
T Message,
ExceptionInfo Exception,
int Attempts,
DateTimeOffset FailedAt,
string? CorrelationId,
Guid ConversationId,
string? TenantId,
string? Source,
IReadOnlyDictionary<string, string?> Headers
) where T : class;Message— the original failing message, exactly as it was deserialized.Exception—ExceptionInforecord withType,Message,StackTrace,InnerException. Inner-exception recursion is depth-capped at 10 to bound payload size.Attempts— how many delivery attempts the failing envelope went through before the terminal decision.FailedAt— the timestamp of the terminal decision (UTC).CorrelationId,ConversationId,TenantId,Source— propagated from the failing envelope.Sourceis the original sender's identity, not the fault publisher's.Headers— copied from the failing envelope withwolverine.encryption.*headers stripped (encryption is decided fresh on the outbound fault hop).
The static FaultHeaders class exposes three constants set on every auto-published fault envelope:
public static class FaultHeaders
{
public const string AutoPublished = "wolverine.fault.auto";
public const string OriginalId = "wolverine.fault.original_id";
public const string OriginalType = "wolverine.fault.original_type";
}AutoPublished is set to "true" on auto-published faults only. Hand-published faults (bus.PublishAsync(new Fault<T>(...))) do not carry this header — useful for distinguishing the two in subscribers and tests. OriginalId and OriginalType carry the failing envelope's ID and Wolverine message-type name so trace consumers can correlate without inspecting the fault body.
Delivery semantics and scope
A fault is published when:
- Moved to error queue (DLQ) — every retry policy that ends in DLQ.
- Discarded — only when the failure rule was configured with
discardWithFaultPublish: true. - Expired envelope — handler entry observes the envelope past its
DeliverBy; counts as a terminal failure.
A fault is not published in these bypass paths:
- Send-side failures — the broker rejects the outbound publish before it ever reached a handler.
- Unknown message type at the receiver — Wolverine cannot synthesize a
Tto wrap. - Pre-handler crypto failures —
EncryptionPolicyViolationException,EncryptionMissingHeaderException,EncryptionDecryptionExceptionshort-circuit before the handler runs. - Fault-publish recursion — a failing
Fault<T>handler will not trigger aFault<Fault<T>>. The recursion guard logs at Debug and emits awolverine.fault.recursion_suppressedactivity event.
Atomicity caveat. Fault publish is best-effort, not transactionally co-committed with the DLQ insert. The receive-side outbox does not enrol the fault publish in the same transaction as the DLQ row. In the unlikely window where the DLQ commit succeeds but the fault enqueue throws, the fault is dropped (logged at Error and the failure counter is incremented). Subscribers must therefore be resilient to gaps; they cannot use Fault events as a strict audit log.
Subscribing to faults
Standard handler discovery applies — write a method named Handle / HandleAsync / Consume / ConsumeAsync taking Fault<T> for each T you care about. Routing for the fault envelope uses the global routing graph; persistence uses the same outbox/inbox you configured for any other message.
A test-friendly subscriber distinguishes auto-published from hand-published faults:
public static class OrderPlacedFaultHandler
{
public static void Handle(Fault<OrderPlaced> fault, Envelope envelope)
{
var auto = envelope.Headers.TryGetValue(FaultHeaders.AutoPublished, out var v)
&& v == "true";
Console.WriteLine($"Order {fault.Message.Id} {(auto ? "auto-faulted" : "manually faulted")}");
}
}Naming convention. Wolverine's conventional handler discovery requires class names ending in
HandlerorConsumer(or[WolverineHandler]on the class). A class namedOrderPlacedFaultSinkwill not be discovered automatically.
Per-type fault configuration
Override the global mode and redaction on a single message type:
opts.Policies.ForMessagesOfType<OrderPlaced>()
.PublishFault(includeExceptionMessage: true, includeStackTrace: false);
opts.Policies.ForMessagesOfType<HighVolumeChatter>()
.DoNotPublishFault();Override semantics:
- A per-type override is fully specified — Mode and redaction never partially inherit from globals. Calling
PublishFault()with no parameters setsincludeExceptionMessage = true, includeStackTrace = true(the parameter defaults), even ifPublishFaultEvents(includeExceptionMessage: false)was set globally. Always pass the redaction flags explicitly when overriding. - Calls must happen before host startup.
WolverineRuntime.StartAsynccallsFaultPublishingPolicy.Freeze(); later attempts to add or change overrides throwInvalidOperationException.
Fault redaction
Two flags on PublishFaultEvents (global) and matching parameters on PublishFault (per-type):
opts.PublishFaultEvents(
includeExceptionMessage: false,
includeStackTrace: false);What gets redacted:
Fault<T>.Exception.Message→string.EmptyFault<T>.Exception.StackTrace→null- Recurses through
InnerExceptionandAggregateException.InnerExceptions. ExceptionInfo.Typeis always preserved (type names are in source code anyway).- Headers,
Source, correlation/conversation/tenant IDs are never redacted.
Note: redaction targets
Fault<T>.Exceptiononly. The original message instanceTcarried asFault<T>.Messageis unchanged — that is what fault events are for. IfTitself is sensitive, the per-type encryption pairing (next section) is the right tool.
Fault encryption pairing
Calling Policies.ForMessagesOfType<T>().Encrypt() automatically registers the encrypting serializer rule for Fault<T> and adds typeof(Fault<T>) to the receive-side encryption requirement set. No manual setup. Skipped for value-type T because Fault<T> requires T : class.
See Message Encryption → Fault events for the byte-level mechanics, the wolverine.encryption.* header strip, and the receive-side RequireEncryption() interaction. Note in particular that RequireEncryption() is a receive-side guard only — it does not constrain the outbound republish of an auto-published Fault<T> triggered by failures on that listener.
Fault observability
Three Activity events are added to the failing envelope's span:
wolverine.fault.published— when a fault is enqueued for routing.wolverine.fault.no_route— when no route exists forFault<T>. Tagged withwolverine.fault.message_type.wolverine.fault.recursion_suppressed— when the recursion guard fires.
One counter:
wolverine.fault.events_published—Counter<int>, incremented per fault enqueued. Suppressed (recursion-guarded) faults do not increment this counter.
On publish failure (the MUST NOT throw contract): the publisher catches, logs at Error, sets the activity status to Error, and emits a wolverine.fault.publish_failed activity event.
Outbound Fault<T> envelopes inherit ConversationId, CorrelationId, and TraceParent from the failing envelope, so distributed traces stay connected across the failure → fault hop.
Testing fault events with ITrackedSession
The tracked-session API surfaces auto-published faults so test assertions don't have to subscribe explicitly:
var tracked = await host.TrackActivity()
.DoNotAssertOnExceptionsDetected()
.SendMessageAndWaitAsync(new OrderPlaced(...));
var faults = tracked.AutoFaultsPublished.OfType<Fault<OrderPlaced>>().ToArray();
faults.ShouldHaveSingleItem();Hand-published bus.PublishAsync(new Fault<T>(...)) calls do not appear in AutoFaultsPublished — only auto-header'd ones do. This lets tests distinguish unintended auto-publishes from intentional manual ones.
Fault event pitfalls
Fault<T>.ToString()leaksMessageplaintext. Positional records auto-generate aToStringthat includes every field. Logging$"Got {fault}"writes the wrappedTplaintext into your log sink. For sensitiveT, use the encryption pairing AND avoid logging the fault directly.RequireEncryption()does not constrain outbound faults. Marking a listener.RequireEncryption()only rejects unencrypted inbound envelopes on that listener; it has no effect on whether aFault<T>triggered by a failure on that listener is encrypted on its outbound hop. Use the per-typeEncrypt()pairing for outbound protection.- Manual
bus.PublishAsync(new Fault<T>(...))skips the auto-header. That is by design — assertions andITrackedSession.AutoFaultsPublisheddistinguish auto from manual. Filtering onFaultHeaders.AutoPublishedexcludes manual publishes. - Recursion suppression is silent in metrics. A suppressed recursive fault does NOT increment
wolverine.fault.events_published. Watch for thewolverine.fault.recursion_suppressedactivity event if you suspect a Fault-handler is faulting. - Per-type override defaults are NOT global defaults.
Policies.ForMessagesOfType<T>().PublishFault()with no parameters uses the parameter defaults (true/true), independent of the globalPublishFaultEvents(...)redaction settings. Always pass the redaction flags explicitly when overriding.
See also
- Message Encryption → Fault events — byte-level encryption interaction.
FaultEventsDemo— runnable single-process sample.

