Microsoft Orleans is an open-source .NET framework for building distributed, stateful applications with the virtual actor model: you write plain C# classes called grains, and the runtime decides where they live, when they load and how they survive failures. This guide is for .NET developers and architects who need low-latency, stateful services such as games, IoT backends, real-time analytics or per-user sessions. It covers how Orleans 10 works, clustering, persistence, timers and reminders, streams, transactions, placement, the concurrency model, observability and how Orleans compares with Akka.NET and Dapr actors.

What Is Microsoft Orleans?#

Orleans came out of Microsoft Research and now runs parts of Azure, Xbox, Skype, Halo, PlayFab and Gears of War, according to its documentation. Its central idea is the virtual actor. A classic actor must be created, supervised and eventually stopped by your code. A grain, by contrast, always exists logically: you get a reference from its type and key, call a method, and the runtime activates an instance in memory if none is running. Idle grains are collected and reactivated on demand, possibly on another server, and grain references stay valid across restarts.

That model removes most distributed-systems ceremony. You do not write locks, because each activation processes one request at a time. You do not route requests, because the runtime tracks where every activation lives. You do not write cache-aside code for hot entities, because an active grain is effectively an in-memory, single-writer cache in front of its storage.

The current release line is Orleans 10. Version 10.0 shipped in January 2026 with a built-in dashboard and stable Redis providers, and 10.3 followed in August 2026 with more streaming and storage providers and an incremental source generator. The packages ship builds for .NET 8 and .NET 10, so Orleans runs on .NET 8, 9 and 10. Orleans 9 introduced full cancellation token support for grain methods, a strongly consistent grain directory, memory-based activation shedding and much faster failure detection: 90 seconds instead of 10 minutes by default.

How Orleans Works: Grains, Silos and the Virtual Actor Model#

A grain has a stable identity (a string, GUID, integer or compound key), behavior defined by a grain interface, and optional persistent state. A silo is a host process, usually your ASP.NET Core or worker app, that runs grain activations. Several silos form a cluster, coordinated through a membership table stored in a clustering provider such as Azure Table Storage, Redis, Cosmos DB or an ADO.NET database.

When code calls a grain, Orleans looks up the grain in a distributed directory. If no activation exists, the placement strategy picks a silo, the silo creates the activation, loads persistent state and runs OnActivateAsync, and then the call executes. Calls are asynchronous messages with a default timeout of 30 seconds. Activations that stay idle for 15 minutes, the default collection age, are deactivated to free memory.

Silos probe each other continuously. When a silo dies, the membership protocol declares it dead, its directory entries are removed, and the next call to any of its grains simply activates a new instance elsewhere, reloading the last persisted state. Anything held only in memory since the last write is lost, which is the main durability trade-off you design around.

Getting Started with Orleans 10#

The fastest way to learn Orleans is to co-host a silo inside an ASP.NET Core app. Microsoft.Orleans.Server brings the runtime, the code generator and in-memory storage; reminders live in a separate package.

Bash
dotnet new web -n Telemetry
dotnet add Telemetry package Microsoft.Orleans.Server
dotnet add Telemetry package Microsoft.Orleans.Reminders
C#
var builder = WebApplication.CreateBuilder(args);

// Co-host a silo in the web app: HTTP endpoints and grains share one process
builder.UseOrleans(silo =>
{
    silo.UseLocalhostClustering();         // single-node development cluster
    silo.AddMemoryGrainStorage("devices"); // swap for Azure Table, Cosmos DB or Redis
    silo.UseInMemoryReminderService();     // development only
});

var app = builder.Build();

app.MapPost("/devices/{id}/readings", async (
    string id, Reading reading, IGrainFactory grains, CancellationToken ct) =>
{
    await grains.GetGrain<IDeviceGrain>(id).RecordAsync(reading, ct);
    return Results.Accepted();
});

app.MapGet("/devices/{id}", async (string id, IGrainFactory grains, CancellationToken ct) =>
    Results.Ok(await grains.GetGrain<IDeviceGrain>(id).GetStatusAsync(ct)));

app.Run();

Grain interfaces declare the contract, and every type that crosses a grain boundary needs a generated serializer. Records get implicit member IDs from their primary constructor, while classes use explicit [Id] attributes. [Alias] gives types a stable wire name, so you can rename or move them later without breaking stored data or rolling upgrades.

C#
using Orleans.Runtime;

public interface IDeviceGrain : IGrainWithStringKey
{
    Task RecordAsync(Reading reading, CancellationToken ct = default);
    Task<DeviceStatus> GetStatusAsync(CancellationToken ct = default);
}

[GenerateSerializer, Alias("reading")]
public sealed record Reading(double Temperature, DateTimeOffset At);

[GenerateSerializer, Alias("device-status")]
public sealed record DeviceStatus(string DeviceId, double? Temperature, DateTimeOffset? LastSeen);

[GenerateSerializer, Alias("device-state")]
public sealed class DeviceState
{
    [Id(0)] public double? Temperature { get; set; }
    [Id(1)] public DateTimeOffset? LastSeen { get; set; }
    [Id(2)] public long ReadingCount { get; set; }
}

Grain Persistence, Timers and Reminders#

Grains declare persistent state through constructor parameters marked with [PersistentState], naming the state and the storage provider. Orleans reads state before OnActivateAsync runs, but it never writes automatically: you call WriteStateAsync when you decide the change must be durable. Providers use ETags for optimistic concurrency and throw InconsistentStateException if another writer got there first, which usually signals a duplicate activation after a network partition. A grain can hold several named states in different stores.

Timers run callbacks inside an activation and die with it, which makes them ideal for batching and housekeeping. RegisterGrainTimer replaced the older RegisterTimer API and supports cancellation, optional interleaving and a keep-alive flag. Reminders are persisted in a reminder service, fire even when the grain is not active and survive silo restarts, but their periods are measured in minutes rather than milliseconds. The grain below uses both: a timer for write-behind batching and a reminder for a durable offline check.

C#
using Orleans.Runtime;

public sealed class DeviceGrain(
    [PersistentState("state", "devices")] IPersistentState<DeviceState> device,
    ILogger<DeviceGrain> logger) : Grain, IDeviceGrain, IRemindable
{
    private bool _dirty;

    public override Task OnActivateAsync(CancellationToken ct)
    {
        // Volatile timer: batch writes while the activation is alive
        this.RegisterGrainTimer(FlushAsync, TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(10));
        return Task.CompletedTask;
    }

    public async Task RecordAsync(Reading reading, CancellationToken ct)
    {
        device.State.Temperature = reading.Temperature;
        device.State.LastSeen = reading.At;
        _dirty = true;

        if (++device.State.ReadingCount == 1)
        {
            // Durable reminder: fires even if the grain is deactivated or a silo restarts
            await this.RegisterOrUpdateReminder(
                "offline-check", TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(5));
            await FlushAsync(ct); // write the first reading immediately
        }
    }

    public Task<DeviceStatus> GetStatusAsync(CancellationToken ct) => Task.FromResult(
        new DeviceStatus(
            this.GetPrimaryKeyString(), device.State.Temperature, device.State.LastSeen));

    public Task ReceiveReminder(string reminderName, TickStatus status)
    {
        if (device.State.LastSeen < DateTimeOffset.UtcNow.AddMinutes(-15))
        {
            logger.LogWarning("Device {DeviceId} looks offline", this.GetPrimaryKeyString());
        }

        return Task.CompletedTask;
    }

    public override Task OnDeactivateAsync(DeactivationReason reason, CancellationToken ct) =>
        FlushAsync(ct); // best effort: a crash still loses the last unsaved batch

    private async Task FlushAsync(CancellationToken ct)
    {
        if (!_dirty)
        {
            return;
        }

        await device.WriteStateAsync(ct);
        _dirty = false;
    }
}

Write-behind batching is a deliberate trade: it cuts storage writes dramatically, but a silo crash loses up to one batch. For money, orders or anything you cannot recompute, write before you return. Orleans 10.x also ships Durable Jobs, positioned as the next generation of reminders for scheduled work, but its packages are still alpha. For audit trails and replayable history, Orleans offers event-sourced grains built on JournaledGrain, which pairs well with event sourcing patterns.

Clustering Providers and Production Silo Configuration#

Every production cluster needs a membership provider. Orleans ships providers for Azure Table Storage, Azure Cosmos DB, Redis, ADO.NET (SQL Server, PostgreSQL, MySQL and others), Amazon DynamoDB, Consul, ZooKeeper and Cassandra. Choose the store your platform team already operates well, because an unavailable membership table stops silos from joining. Two identifiers matter: ServiceId must stay constant for the life of the application because storage and reminders are keyed by it, while ClusterId identifies one deployment and can change between blue-green releases.

On Kubernetes, UseKubernetesHosting from Microsoft.Orleans.Hosting.Kubernetes sets the silo name and address from the pod, reads the cluster and service IDs from pod labels, and marks silos of deleted pods as dead. It does not replace the clustering provider. Silos listen on port 11111 for silo traffic and 30000 for gateway clients by default, and the Orleans docs recommend a generous terminationGracePeriodSeconds so silos can hand off activations. See Running .NET on Kubernetes for the broader deployment picture.

C#
using Azure.Identity;
using Orleans.Configuration;

var builder = WebApplication.CreateBuilder(args);
var tables = new Uri(builder.Configuration["Orleans:TableEndpoint"]!);
var credential = new DefaultAzureCredential(); // managed identity in Azure, no secrets

builder.UseOrleans(silo =>
{
    silo.Configure<ClusterOptions>(options =>
    {
        options.ClusterId = "telemetry-blue"; // one deployment
        options.ServiceId = "telemetry";      // never changes: keys storage and reminders
    });

    silo.UseAzureStorageClustering(o => o.ConfigureTableServiceClient(tables, credential));
    silo.AddAzureTableGrainStorage("devices",
        o => o.ConfigureTableServiceClient(tables, credential));
    silo.UseAzureTableReminderService(o => o.ConfigureTableServiceClient(tables, credential));
    silo.AddActivityPropagation(); // flow W3C trace context through grain calls
});

Orleans Streams and Broadcast Channels#

Streams let grains and clients publish and consume ordered sequences of events without knowing about each other. A stream is identified by a namespace and a key and, like a grain, always exists logically. Consumers subscribe explicitly, or declare an implicit subscription with [ImplicitStreamSubscription], in which case Orleans activates a consumer grain whose key matches the stream key as soon as an event arrives. Explicit subscriptions are tracked in a grain storage provider named PubSubStore.

Delivery guarantees and ordering depend on the provider. Azure Event Hubs streams are rewindable and support resuming from a sequence token, while in-memory streams suit tests and development. The 10.x line added AWS Kinesis and SQS FIFO adapters and an alpha NATS provider. For fire-and-forget fan-out to every silo, such as cache invalidation, use broadcast channels instead.

C#
using Orleans.Runtime;
using Orleans.Streams;

// Silo setup: silo.AddMemoryStreams("telemetry").AddMemoryGrainStorage("PubSubStore");
public sealed class IngestGrain : Grain, IIngestGrain
{
    public Task PublishAsync(Reading reading) =>
        this.GetStreamProvider("telemetry")
            .GetStream<Reading>(StreamId.Create("readings", this.GetPrimaryKey()))
            .OnNextAsync(reading);
}

// One consumer activation per stream key, created on demand by the runtime
[ImplicitStreamSubscription("readings")]
public sealed class AnomalyGrain(ILogger<AnomalyGrain> logger) : Grain, IAnomalyGrain
{
    private double _average;

    public override async Task OnActivateAsync(CancellationToken ct)
    {
        var stream = this.GetStreamProvider("telemetry")
            .GetStream<Reading>(StreamId.Create("readings", this.GetPrimaryKey()));
        await stream.SubscribeAsync(OnReadingAsync);
    }

    private Task OnReadingAsync(Reading reading, StreamSequenceToken? token)
    {
        _average = _average == 0 ? reading.Temperature : _average * 0.9 + reading.Temperature * 0.1;
        if (Math.Abs(reading.Temperature - _average) > 10)
        {
            logger.LogWarning("Anomaly on {Device}: {Value}",
                this.GetPrimaryKey(), reading.Temperature);
        }

        return Task.CompletedTask;
    }
}

public interface IIngestGrain : IGrainWithGuidKey
{
    Task PublishAsync(Reading reading);
}

public interface IAnomalyGrain : IGrainWithGuidKey { }

Distributed ACID Transactions Across Grains#

Orleans supports distributed transactions with serializable isolation across any number of grains, without a central coordinator. You enable them on the silo with UseTransactions, register transactional state storage, mark interface methods with [Transaction] to say whether a call creates or joins a transaction, and access state only through PerformRead and PerformUpdate. An exception inside the transaction aborts it and rolls back every participant.

Transactions cost extra storage round trips and lock contention, so use them where invariants span grains, such as transfers between accounts or inventory moves. For most workflows, a single grain that owns the invariant, or a saga with compensations, is cheaper.

C#
using Orleans.Concurrency;
using Orleans.Transactions.Abstractions;

// Silo: silo.UseTransactions().AddAzureTableTransactionalStateStorage("tx", o => ...);
public interface IWalletGrain : IGrainWithStringKey
{
    [Transaction(TransactionOption.Join)] Task DebitAsync(decimal amount);
    [Transaction(TransactionOption.Join)] Task CreditAsync(decimal amount);
    [Transaction(TransactionOption.CreateOrJoin)] Task<decimal> GetBalanceAsync();
}

[GenerateSerializer]
public sealed class Balance
{
    [Id(0)] public decimal Value { get; set; }
}

[Reentrant]
public sealed class WalletGrain(
    [TransactionalState("balance", "tx")] ITransactionalState<Balance> balance)
    : Grain, IWalletGrain
{
    public Task DebitAsync(decimal amount) => balance.PerformUpdate(state =>
    {
        if (state.Value < amount)
        {
            throw new InvalidOperationException("Insufficient funds."); // aborts everything
        }

        state.Value -= amount;
    });

    public Task CreditAsync(decimal amount) =>
        balance.PerformUpdate(state => { state.Value += amount; });

    public Task<decimal> GetBalanceAsync() => balance.PerformRead(state => state.Value);
}

// Program.cs (caller): both wallets change, or neither does
app.MapPost("/transfers", async (Transfer t, IGrainFactory grains, ITransactionClient tx) =>
{
    await tx.RunTransaction(TransactionOption.Create, async () =>
    {
        await grains.GetGrain<IWalletGrain>(t.From).DebitAsync(t.Amount);
        await grains.GetGrain<IWalletGrain>(t.To).CreditAsync(t.Amount);
    });
    return Results.NoContent();
});

public sealed record Transfer(string From, string To, decimal Amount);

Placement, Reentrancy and the Orleans Concurrency Model#

By default, a grain activation is single-threaded and non-reentrant: it processes each request from start to finish, and while it awaits another grain, other requests to it queue up. This is what makes grain code lock-free, but it has a sharp edge. If grain A calls grain B while B is calling A, both wait on each other until the 30-second timeout fires. Break cycles by design, or relax the rules deliberately:

  • [Reentrant] on a class lets any request interleave at await points, so state can change across an await.
  • [AlwaysInterleave] on an interface method lets that method run alongside anything, which is useful for health checks and cancellation calls.
  • [ReadOnly] lets read-only methods run concurrently with each other.
  • [MayInterleave] makes the decision per call through a predicate, and RequestContext.AllowCallChainReentrancy allows reentrancy for one call chain only.
  • [StatelessWorker] allows several local activations of a stateless grain, which suits CPU-bound work and fan-out.

Placement decides which silo hosts a new activation. Since Orleans 9.2 the default is resource-optimized placement, which weighs CPU, memory and activation counts. Before that it was random placement. Attributes select other strategies per grain class: prefer-local, hash-based, activation-count-based and silo-role-based. Orleans 10 added placement filtering for zone-aware or hardware-specific placement, and Orleans can also rebalance activations or migrate chatty grains closer together, features you should enable only after measuring.

C#
using Orleans.Concurrency;
using Orleans.Placement;

public interface IPriceListGrain : IGrainWithStringKey
{
    [ReadOnly] Task<decimal> GetPriceAsync(string sku);   // concurrent with other reads
    Task SetPriceAsync(string sku, decimal price);         // exclusive turn
    [AlwaysInterleave] Task<bool> PingAsync();             // never queued behind slow calls
}

public interface IThumbnailGrain : IGrainWithIntegerKey
{
    Task<byte[]> RenderAsync(byte[] image);
}

// Up to 4 activations per silo, always local: good for stateless, CPU-bound work
[StatelessWorker(4)]
public sealed class ThumbnailGrain : Grain, IThumbnailGrain
{
    // ImageTools stands in for your own CPU-bound code
    public Task<byte[]> RenderAsync(byte[] image) => Task.FromResult(ImageTools.Shrink(image));
}

public interface ISessionGrain : IGrainWithStringKey
{
    Task TouchAsync();
}

// Keep per-connection session grains on the silo that received the request
[PreferLocalPlacement]
public sealed class SessionGrain : Grain, ISessionGrain
{
    public Task TouchAsync() => Task.CompletedTask;
}

Observability: Metrics, Tracing and the Orleans Dashboard#

Orleans publishes metrics through the Microsoft.Orleans meter, covering activations, messaging, storage latency and reminders. Tracing uses activity sources named Microsoft.Orleans.Application for grain calls made by your code, plus Microsoft.Orleans.Runtime, .Lifecycle, .Storage and .DurableJobs for runtime internals. Call AddActivityPropagation on silos and clients so trace context flows across grain calls, then export everything with OpenTelemetry. Version 10.3 aligned RPC span attributes with the OpenTelemetry semantic conventions, so update dashboards and queries that relied on the older keys.

Orleans 10 also added an official dashboard package, Microsoft.Orleans.Dashboard, which shows silos, activations, method profiling, reminders and live logs. It exposes internal details, so always protect it with ASP.NET Core authorization.

C#
using Orleans.Dashboard;

builder.UseOrleans(silo =>
{
    silo.UseLocalhostClustering();
    silo.AddActivityPropagation();
    silo.AddDashboard(); // package: Microsoft.Orleans.Dashboard
});

builder.Services.AddOpenTelemetry()
    .WithMetrics(metrics => metrics
        .AddMeter("Microsoft.Orleans")
        .AddOtlpExporter())
    .WithTracing(tracing => tracing
        .AddSource("Microsoft.Orleans.Application") // your grain calls
        .AddAspNetCoreInstrumentation()
        .AddOtlpExporter());

var app = builder.Build();
app.MapOrleansDashboard(routePrefix: "/dashboard").RequireAuthorization();

Running Orleans with .NET Aspire#

.NET Aspire has first-class Orleans support through the Aspire.Hosting.Orleans package. The AppHost describes the cluster and its backing resources, then injects configuration into silo and client projects, so UseOrleans() needs no provider code. Silo projects must still register the matching keyed client, such as AddKeyedRedisClient, so Orleans providers can resolve the connection at runtime.

C#
// AppHost/Program.cs
var builder = DistributedApplication.CreateBuilder(args);

var redis = builder.AddRedis("orleans-redis");
var orleans = builder.AddOrleans("cluster")
    .WithClustering(redis)
    .WithGrainStorage("devices", redis)
    .WithReminders(redis);

builder.AddProject<Projects.Telemetry_Silo>("silo")
    .WithReference(orleans)
    .WaitFor(redis)
    .WithReplicas(3);

builder.AddProject<Projects.Telemetry_Api>("api")
    .WithReference(orleans.AsClient()) // gateway client, hosts no grains
    .WaitFor(redis);

builder.Build().Run();

// Silo/Program.cs
var siloBuilder = Host.CreateApplicationBuilder(args);
siloBuilder.AddServiceDefaults();
siloBuilder.AddKeyedRedisClient("orleans-redis");
siloBuilder.UseOrleans(); // clustering, storage and reminders come from the AppHost
siloBuilder.Build().Run();

Real-World Use Cases for Orleans#

Orleans fits systems with many independent, stateful entities that receive frequent, low-latency requests:

  • Games and live services: player profiles, sessions, lobbies and matchmaking, the workloads Orleans was originally built for.
  • IoT and digital twins: one grain per device holding the latest telemetry, rules and alerts, fed by streams.
  • Real-time analytics: counters, leaderboards and windows maintained in memory and flushed periodically.
  • Commerce: carts, inventory reservations and pricing, with transactions where invariants span entities.
  • Collaboration and presence: documents, rooms and user presence with push notifications through observers or SignalR.
  • AI assistants: per-user or per-conversation grains that keep context in memory and serialize access to an agent's state.

It is a poor fit for large analytical queries across entities, since there is no query across grains, and for simple CRUD services where a database and a stateless API are enough.

Best Practices#

  • Model grains around consistency boundaries. One grain per account, device or session keeps invariants local and avoids transactions.
  • Avoid hot grains. A single global grain becomes a throughput bottleneck. Use stateless workers or hierarchical aggregation for counters and fan-in.
  • Decide when state becomes durable. Call WriteStateAsync before acknowledging critical operations, and batch only data you can afford to lose.
  • Version your contracts. Use [Alias] on types and methods, never reuse [Id] values, and roll out interface changes in backward-compatible steps.
  • Flow cancellation tokens. Orleans 9 and later honor tokens in grain methods, so pass them from HTTP requests down to storage calls.
  • Run at least three silos in production with a durable clustering provider, and keep ServiceId stable forever.
  • Instrument from day one. Export the Microsoft.Orleans meter and application traces, and alert on activation counts, request latency and storage errors.
  • Test with a real cluster. The Microsoft.Orleans.TestingHost package runs multi-silo test clusters in-process, which catches serialization and reentrancy bugs early.

Common Pitfalls#

  • Call cycles. A calls B, B calls A, and both time out after 30 seconds. Redesign the flow, use [AlwaysInterleave] for callbacks, or allow call-chain reentrancy.
  • Blocking inside grains. .Result and .Wait() block the activation's scheduler and can stall the silo. Stay async all the way.
  • Using timers for durable work. Timers vanish on deactivation. Use reminders, or an external queue, for anything that must happen.
  • Assuming in-memory state is safe. A silo crash reverts grains to their last persisted state. Design for that.
  • Unversioned serialization. Types without generated serializers or with reused IDs break rolling upgrades and stored state. Also note that Orleans 10.3 hardened the default JSON grain storage with type allow-lists, so polymorphic state can need configuration after upgrading.
  • Treating Orleans as a database. You cannot query across grains. Project state into a database or search index for reporting.
  • Exposing the dashboard. It reveals grain types, silos and logs. Put it behind authentication and never on a public endpoint.

Orleans vs Akka.NET vs Dapr Actors#

All three bring the actor model to .NET, but they make different trade-offs. Dapr actors run behind a sidecar and are callable from any language, while Orleans and Akka.NET are in-process libraries.

AspectOrleansAkka.NETDapr actors
Actor styleVirtual actors, activated on demandClassic actors with explicit creation and supervision; sharding adds on-demand entitiesVirtual actors behind a sidecar
Programming modelTyped C# interfaces with async methodsMessage passing with Tell and AskTyped .NET proxies, or HTTP and gRPC
Languages.NETC# and F#Any language
ConcurrencySingle-threaded turns, opt-in interleavingOne message at a time per mailboxTurn-based, opt-in reentrancy
StateGrain storage, event sourcing, ACID transactionsAkka.Persistence event sourcing and snapshotsDapr state store component
StreamingOrleans streams, broadcast channelsAkka.StreamsDapr pub/sub building block
Durable schedulingReminders; Durable Jobs in alphaIn-memory scheduler, durability via persistenceReminders in the Scheduler service
Runs asLibrary in your .NET hostLibrary in your .NET hostSidecar plus control plane
LicenseMITApache 2.0Apache 2.0

Choose Orleans for .NET-only systems that want the highest productivity and rich built-in features. Choose Akka.NET when you want explicit supervision hierarchies, stream processing with back-pressure, or a design shared with the JVM Akka ecosystem. Choose Dapr actors when services in several languages need to call the same actors, or when you already run Dapr. For interview-style trade-off discussions, see the actor model interview questions.

Frequently Asked Questions#

Is Microsoft Orleans production-ready?#

Yes. Orleans has powered large Microsoft services such as Azure, Xbox, Halo and PlayFab for years, and it is MIT-licensed and actively developed, with 10.3 released in August 2026. Production readiness still depends on your choices: a durable clustering provider, persisted state for critical data, and monitoring.

What is the difference between a grain and a silo?#

A grain is a virtual actor: an addressable entity with an identity, behavior and optional state. A silo is a host process that runs grain activations. A cluster of silos shares the grain workload, and the runtime moves activations between silos as membership changes.

Does Orleans require Azure?#

No. Orleans runs anywhere .NET runs, including Kubernetes, VMs and other clouds. Azure Storage and Cosmos DB are popular providers, but Redis, ADO.NET databases, Amazon DynamoDB, Consul and ZooKeeper are all supported.

What happens to grains when a silo fails?#

The membership protocol detects the failure (within about 90 seconds by default since Orleans 9), and the directory forgets the dead silo's activations. The next call to any affected grain activates it on a healthy silo and reloads its last persisted state. Unsaved in-memory changes are lost, and in-flight calls fail with timeouts or exceptions that callers should handle.

Should I use Orleans or Dapr actors?#

Use Orleans when your services are .NET and you want in-process performance, typed interfaces, streams, transactions and flexible placement. Use Dapr actors when non-.NET services must call the actors, or when Dapr already provides your pub/sub, state and workflow building blocks. The concepts transfer, so the choice is mostly about platform and ecosystem.

Summary#

  • Orleans implements virtual actors: grains always exist logically, activate on demand and process one request at a time, so you write lock-free, location-transparent C#.
  • Silos form a cluster through a membership provider; persist state explicitly with IPersistentState<T> and keep ServiceId stable.
  • Use timers for in-memory housekeeping, reminders for durable schedules, streams for event flows, and transactions only where invariants span grains.
  • Understand the concurrency model: non-reentrant by default, with attributes to relax it, and placement that defaults to resource-optimized since 9.2.
  • Orleans 10 adds an official dashboard, stable Redis providers and new streaming providers, and it integrates cleanly with OpenTelemetry and Aspire.

Further Reading#