Help us improve
Share bugs, ideas, or general feedback.
From dotnet-skills
Provides Akka.NET best practices for EventStream vs DistributedPubSub, supervision strategies, error handling, Props vs DependencyResolver, work distribution, and cluster/local testability abstractions.
npx claudepluginhub aaronontheweb/dotnet-skills --plugin dotnet-skillsHow this skill is triggered — by the user, by Claude, or both
Slash command
/dotnet-skills:akka-best-practicesThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Use this skill when:
Provides Akka.Hosting patterns for entity actors supporting local testing (GenericChildPerEntityParent) and clustered production (sharding abstraction), with message extractors, reminders, and ITimeProvider.
Covers asynchronous messaging patterns in .NET with Wolverine and MassTransit: outbox pattern, saga/choreography, and broker config for RabbitMQ and Azure Service Bus.
Guides implementation of circuit breaker, retry, DLQ, timeout, bulkhead, and fallback patterns for .NET using Polly for HTTP clients and Brighter for message handlers to handle transient failures.
Share bugs, ideas, or general feedback.
Use this skill when:
Context.System.EventStream is local to a single ActorSystem process. It does NOT work across cluster nodes.
// BAD: This only works on a single server
// When you add a second server, subscribers on server 2 won't receive events from server 1
Context.System.EventStream.Subscribe(Self, typeof(PostCreated));
Context.System.EventStream.Publish(new PostCreated(postId, authorId));
When EventStream is appropriate:
For events that must reach actors across multiple cluster nodes, use Akka.Cluster.Tools.PublishSubscribe:
using Akka.Cluster.Tools.PublishSubscribe;
public class TimelineUpdatePublisher : ReceiveActor
{
private readonly IActorRef _mediator;
public TimelineUpdatePublisher()
{
// Get the DistributedPubSub mediator
_mediator = DistributedPubSub.Get(Context.System).Mediator;
Receive<PublishTimelineUpdate>(msg =>
{
// Publish to a topic - reaches all subscribers across all nodes
_mediator.Tell(new Publish($"timeline:{msg.UserId}", msg.Update));
});
}
}
builder.WithDistributedPubSub(role: null); // Available on all roles, or specify a role
| Pattern | Topic Format | Use Case |
|---|---|---|
| Per-user | timeline:{userId} | Timeline updates, notifications |
| Per-entity | post:{postId} | Post engagement updates |
| Broadcast | system:announcements | System-wide notifications |
| Role-based | workers:rss-poller | Work distribution |
A supervision strategy defined on an actor dictates how that actor supervises its children, NOT how the actor itself is supervised.
public class ParentActor : ReceiveActor
{
// This strategy applies to children of ParentActor, NOT to ParentActor itself
protected override SupervisorStrategy SupervisorStrategy()
{
return new OneForOneStrategy(
maxNrOfRetries: 10,
withinTimeRange: TimeSpan.FromSeconds(30),
decider: ex => ex switch
{
ArithmeticException => Directive.Resume,
NullReferenceException => Directive.Restart,
ArgumentException => Directive.Stop,
_ => Directive.Escalate
});
}
}
The default OneForOneStrategy already includes rate limiting:
You rarely need a custom strategy unless you have specific requirements.
Good reasons:
AllForOneStrategyBad reasons:
Use try-catch when:
public class RssFeedPollerActor : ReceiveActor
{
public RssFeedPollerActor()
{
ReceiveAsync<PollFeed>(async msg =>
{
try
{
var feed = await _httpClient.GetStringAsync(msg.FeedUrl);
var items = ParseFeed(feed);
// Process items...
}
catch (HttpRequestException ex)
{
// Expected failure - log and schedule retry
_log.Warning("Feed {Url} unavailable: {Error}", msg.FeedUrl, ex.Message);
Context.System.Scheduler.ScheduleTellOnce(
TimeSpan.FromMinutes(5), Self, msg, Self);
}
catch (XmlException ex)
{
// Invalid feed format - log and mark as bad
_log.Error("Feed {Url} has invalid format: {Error}", msg.FeedUrl, ex.Message);
Sender.Tell(new FeedPollResult.InvalidFormat(msg.FeedUrl));
}
});
}
}
Let exceptions propagate (trigger supervision) when:
// BAD: Swallowing exceptions hides problems
catch (Exception ex)
{
_log.Error(ex, "Error processing work");
// Actor continues with potentially corrupt state
}
// GOOD: Handle known exceptions, let unknown ones propagate
catch (HttpRequestException ex)
{
// Known, expected failure - handle gracefully
_log.Warning("HTTP request failed: {Error}", ex.Message);
Sender.Tell(new WorkResult.TransientFailure());
}
// Unknown exceptions propagate to supervision
Use Props.Create() when:
IServiceProvider or IRequiredActor<T>// Simple actor with no DI needs
public static Props Props(PostId postId, IPostWriteStore store)
=> Akka.Actor.Props.Create(() => new PostEngagementActor(postId, store));
Use resolver.Props<T>() when:
IServiceProvider to create scoped servicesIRequiredActor<T> to get references to other actors// Registration with DI
builder.WithActors((system, registry, resolver) =>
{
var actor = system.ActorOf(resolver.Props<OrderProcessorActor>(), "order-processor");
registry.Register<OrderProcessorActor>(actor);
});
You almost never need remote deployment. If you're not doing remote deployment (and you probably aren't):
Props.Create(() => new Actor(...)) with closures is fineFor most applications, use cluster sharding instead of remote deployment - it handles distribution automatically.
When you have many background jobs (RSS feeds, email sending, etc.), don't process them all at once - this causes thundering herd problems.
Three patterns to solve this:
FOR UPDATE SKIP LOCKED for natural cross-node distributionSee work-distribution-patterns.md for full code samples.
| Mistake | Why It's Wrong | Fix |
|---|---|---|
| Using EventStream for cross-node pub/sub | EventStream is local only | Use DistributedPubSub |
| Defining supervision to "protect" an actor | Supervision protects children | Understand the hierarchy |
| Catching all exceptions | Hides bugs, corrupts state | Only catch expected errors |
| Always using DependencyResolver | Adds unnecessary complexity | Use plain Props when possible |
| Processing all background jobs at once | Thundering herd, resource exhaustion | Use database queue + rate limiting |
| Throwing exceptions for expected failures | Triggers unnecessary restarts | Return result types, use messaging |
Need to communicate between actors?
├── Same process only? -> EventStream is fine
├── Across cluster nodes?
│ ├── Point-to-point? -> Use ActorSelection or known IActorRef
│ └── Pub/sub? -> Use DistributedPubSub
└── Fire-and-forget to external system? -> Consider outbox pattern
Exception occurred in actor?
├── Expected failure (HTTP timeout, invalid input)?
│ └── Try-catch, handle gracefully, continue
├── State might be corrupt?
│ └── Let supervision restart
├── Unknown cause?
│ └── Let supervision restart
└── Programming error (null ref, bad logic)?
└── Let supervision restart, fix the bug
Creating actor Props?
├── Actor needs IServiceProvider?
│ └── Use resolver.Props<T>()
├── Actor needs IRequiredActor<T>?
│ └── Use resolver.Props<T>()
├── Simple actor with constructor params?
│ └── Use Props.Create(() => new Actor(...))
└── Remote deployment needed?
└── Probably not - use cluster sharding instead
For applications that need to run both in clustered production and local/test environments, use abstraction patterns to toggle between implementations:
AkkaExecutionMode enum - Controls which implementations are used (LocalTest vs Clustered)GenericChildPerEntityParent - Mimics sharding behavior locally using the same IMessageExtractorIPubSubMediator - Abstracts DistributedPubSub for swappable local/cluster implementationsSee cluster-local-abstractions.md for complete implementation code.
In actors, use ILoggingAdapter from Context.GetLogger() instead of DI-injected ILogger<T>:
public class MyActor : ReceiveActor
{
private readonly ILoggingAdapter _log = Context.GetLogger();
public MyActor()
{
Receive<MyMessage>(msg =>
{
_log.Info("Processing message for user {UserId}", msg.UserId);
_log.Error(ex, "Failed to process {MessageType}", msg.GetType().Name);
});
}
}
Why ILoggingAdapter:
Info(), Debug(), Warning(), Error() (not Log* variants)Don't inject ILogger into actors - it bypasses Akka's logging infrastructure.
// Named placeholders for better log aggregation and querying
_log.Info("Order {OrderId} processed for customer {CustomerId}", order.Id, order.CustomerId);
// Prefer named placeholders over positional
// Good: {OrderId}, {CustomerId}
// Avoid: {0}, {1}
When actors launch async operations via PipeTo, those operations can outlive the actor if not properly managed. Key practices:
PostStop()See async-cancellation-patterns.md for complete implementation code.