Skip to content

Grain placement and migration

When a grain isn’t active, Orleans selects a compatible silo and creates an activation there. This process is placement. Callers continue using location-transparent grain references, so placement doesn’t change application call sites.

This article covers application-facing placement configuration. For the runtime algorithms and coordination protocols behind placement and activation movement, see Placement and activation balancing.

Placement, collection, migration, and load shedding solve different problems:

Event or mechanismWhat Orleans does
A call needs an activationRuns placement using the current compatible silos and current placement statistics.
A silo joinsIncludes it in later placement decisions after membership and compatibility information converge. Existing activations continue running on their current silos.
An activation remains idleCollects it after the configured idle period. A later call runs placement again, so the replacement activation can use newly added capacity.
A silo leaves or failsRemoves its activations. Calls are routed to activations on remaining silos or cause replacement activations to be placed there.
Explicit or automatic migration is requestedMoves an eligible live activation after its current work completes. Cluster-wide automatic migration is experimental and opt-in.
Enabled load shedding marks a silo overloadedThe client gateway rejects requests, stream queue flow control pauses reads at its CPU threshold, and resource-optimized placement favors non-overloaded candidates.

A hosting platform or operator controls the silo count. Orleans adapts placement and routing to the resulting membership.

ResourceOptimizedPlacement is the default placement strategy. It uses sampled silo runtime statistics and a power-of-k-choices algorithm to balance new activations. It considers CPU, memory, available memory, activation count, and a preference for the local silo. When load shedding marks silos overloaded, placement favors non-overloaded candidates.

For design background on its resource scoring and signal smoothing, see Resource-based placement with cooperative dual-mode Kalman filtering.

Configure its weights through ResourceOptimizedPlacementOptions:

siloBuilder.Configure<ResourceOptimizedPlacementOptions>(options =>
{
options.CpuUsageWeight = 40;
options.MemoryUsageWeight = 20;
options.AvailableMemoryWeight = 20;
options.ActivationCountWeight = 15;
options.LocalSiloPreferenceMargin = 5;
});

Weights are relative and don’t need to total 100. Keep defaults until measurements show a workload-specific reason to change them.

On scale-out, new silos become candidates for new activations. Long-lived active grains continue running on their current silos. Grain code or an opt-in rebalancing service can request their migration. Idle activation collection gradually makes more grain identities eligible for fresh placement.

On graceful scale-in, the departing silo leaves active membership and deactivates its ordinary activations during shutdown. Subsequent calls reactivate those grain identities on remaining silos. After an abrupt loss, failure detection enables the same replacement placement path. In-flight calls can fail or time out, so callers must follow the application’s retry and idempotency policy.

Scale gradually and preserve headroom for reactivation, state reads, cache warming, and temporarily concentrated traffic. See Capacity planning and scaling and Graceful shutdown and scale-in.

Persisted grain state belongs to the grain identity and remains in the configured storage provider across activation lifetimes. With IPersistentState<T>, Orleans reads configured state before OnActivateAsync when it creates an ordinary replacement activation. Every silo which can host the grain must be able to reach the configured storage provider.

Live activation migration transfers runtime migration state directly to the target, including the in-memory state held by IPersistentState<T>. Application-owned in-memory state which must survive a live move must participate through IGrainMigrationParticipant. Awaited storage writes provide durability across process failure; migration state provides continuity during a live move. See Grain persistence and Activation lifecycle and migration.

Set LoadSheddingEnabled to true to activate load shedding. Crossing either the CPU or memory threshold marks the silo as overloaded, enables client-gateway request rejection, and makes resource-optimized placement favor non-overloaded candidates. Stream providers which use LoadShedQueueFlowController pause queue reads according to CPU usage.

Configure it on every silo and choose thresholds from measured headroom:

siloBuilder.Configure<LoadSheddingOptions>(options =>
{
options.LoadSheddingEnabled = true;
options.CpuThreshold = 90;
options.MemoryThreshold = 85;
});

Use load shedding for admission protection, a hosting-platform autoscaler for cluster capacity, and activation rebalancing or repartitioning for eligible activation movement. Set thresholds below the platform’s hard limits, retain headroom for deactivation and recovery work, and monitor rejection rate with CPU, memory, queueing, and latency signals.

Gateway load shedding rejects incoming requests after CPU or memory crosses its threshold. Stream queue flow control uses CPU thresholds to pause reads. Memory-based activation shedding deactivates selected activations to reduce process memory.

Apply a placement attribute to a grain implementation when its requirements differ from the default:

AttributeBehavior
ResourceOptimizedPlacementAttributeExplicitly selects the default resource-aware strategy.
RandomPlacementAttributeChooses a random compatible silo.
PreferLocalPlacementAttributeUses the local compatible silo when possible.
HashBasedPlacementAttributeMaps the grain ID across the current compatible silo set.
ActivationCountBasedPlacementAttributeFavors sampled silos with fewer activations.
SiloRoleBasedPlacementAttributeRestricts placement by silo role.
StatelessWorkerAttributeUses local, scalable worker-pool placement.

Activation-count-based placement applies the power-of-two-choices technique described in The Power of Two Choices in Randomized Load Balancing.

[PreferLocalPlacement]
public sealed class GatewayCacheGrain :
Grain,
IGatewayCacheGrain
{
}

Placement happens when creating an activation. Changing cluster membership or a strategy doesn’t move existing activations by itself.

Register a different default strategy only when all unannotated grains should use it:

siloBuilder.Services.AddSingleton<
PlacementStrategy,
RandomPlacement>();

Per-grain attributes still take precedence.

Placement filters reduce the compatible candidate set before the placement strategy selects a silo. They can express requirements or preferences based on silo metadata. The built-in metadata filter attributes are experimental and produce diagnostic ORLEANSEXP004.

See Placement filters for the built-in filters and experimental status.

A grain can ask Orleans to move its activation after current work completes:

public Task Move()
{
MigrateOnIdle();
return Task.CompletedTask;
}

Migration is advisory and occurs only if placement chooses another compatible silo. Custom activation state must participate in dehydration and rehydration; see Grain activation and lifecycle.

Use ImmovableAttribute to exclude a grain class from automatic movement:

[Immovable]
public sealed class HardwareSessionGrain :
Grain,
IHardwareSessionGrain
{
}

The attribute doesn’t prevent explicit MigrateOnIdle calls.

Orleans includes two opt-in experimental services:

FeatureGoalDiagnostic
Activation repartitionerImprove grain-to-grain call locality.ORLEANSEXP001
Activation rebalancerBalance activation count and memory pressure across silos.ORLEANSEXP002

Enable them independently:

#pragma warning disable ORLEANSEXP001
siloBuilder.AddActivationRepartitioner();
#pragma warning restore ORLEANSEXP001
#pragma warning disable ORLEANSEXP002
siloBuilder.AddActivationRebalancer();
#pragma warning restore ORLEANSEXP002

Both features migrate eligible activations and can operate together. They add cluster coordination and state-transfer costs, so benchmark representative workloads before production use. Stateless workers, system targets, grain services, client objects, and immovable activations aren’t candidates.

Choose the activation rebalancer when uneven activation count or activation memory is the problem. Choose the activation repartitioner when cross-silo calls between grains are the problem. Enabling both lets the repartitioner’s default tolerance rule incorporate the rebalancer’s view of cluster imbalance. Pair them with a hosting-platform autoscaler for capacity and load shedding for overload admission control. See Placement and activation balancing for tuning and observability details.

Custom placement strategies and directors are advanced runtime extensions. Implement them only when built-in strategies plus placement filters can’t express the requirement. A custom implementation has three parts:

  1. A PlacementStrategy that identifies the policy.
  2. A PlacementAttribute that applies the policy to a grain class.
  3. An IPlacementDirector that selects one candidate silo.

The following example gives related grain types an affinity for the same silo when they use the same grain key. This differs from HashBasedPlacement, which hashes the complete grain ID, including its grain type.

First, define the strategy and its attribute:

[GenerateSerializer, Immutable, SuppressReferenceTracking]
public sealed class CommonKeyPlacementStrategy : PlacementStrategy
{
}
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public sealed class CommonKeyPlacementAttribute()
: PlacementAttribute(new CommonKeyPlacementStrategy());

Placement strategy instances can cross runtime serialization boundaries. Use [GenerateSerializer]; if the strategy adds serializable state, assign stable [Id(n)] values to its members. The strategy in this example is immutable and has no serialized members.

Next, implement the director:

public sealed class CommonKeyPlacementDirector : IPlacementDirector
{
public Task<SiloAddress> OnAddActivation(
PlacementStrategy strategy,
PlacementTarget target,
IPlacementContext context)
{
var compatibleSilos = context.GetCompatibleSilos(target);
if (compatibleSilos.Length == 0)
{
throw new InvalidOperationException(
$"No compatible silo is available for {target.GrainIdentity}.");
}
if (IPlacementDirector.GetPlacementHint(
target.RequestContextData,
compatibleSilos) is { } placementHint)
{
return Task.FromResult(placementHint);
}
var sortedSilos = compatibleSilos.OrderBy(silo => silo).ToArray();
var index = target.GrainIdentity.Key.GetUniformHashCode()
% (uint)sortedSilos.Length;
return Task.FromResult(sortedSilos[index]);
}
}

Call GetCompatibleSilos instead of reconstructing cluster membership. It returns active silos which can host the grain type and satisfy interface-version compatibility, after placement filters have run. The current runtime throws if that process leaves no candidates; the explicit empty-set check also protects the modulo operation in tests or alternate context implementations. GetPlacementHint accepts a request hint only when it names one of those candidates, so the example honors valid hints before applying its own policy.

The director sorts the candidate addresses before indexing them and uses the grain key’s stable, uniform hash. Therefore, two grain types with the same key select the same silo only when they see the same candidate set:

[CommonKeyPlacement]
public sealed class CartGrain : Grain, ICartGrain
{
public Task Ping() => Task.CompletedTask;
}
[CommonKeyPlacement]
public sealed class CartIndexGrain : Grain, ICartIndexGrain
{
public Task Ping() => Task.CompletedTask;
}

This is an affinity, not durable pinning. Membership changes, silo restarts, or different compatibility and filter results can change the mapping. Existing activations don’t move merely because a later placement decision maps elsewhere. The uniform hash is deterministic across cluster nodes but isn’t cryptographic, so don’t use placement as an authorization or isolation boundary.

Finally, register the strategy and director on every silo:

public static void AddCustomPlacement(ISiloBuilder siloBuilder)
{
siloBuilder.Services.AddPlacementDirector<
CommonKeyPlacementStrategy,
CommonKeyPlacementDirector>();
}

This overload registers the stateless strategy and the director as keyed singletons. Other overloads can change the strategy lifetime, but the director remains a keyed singleton. Directors must therefore be thread-safe and use singleton-safe dependencies. If a strategy carries attribute configuration, preserve it through PopulateGrainProperties and Initialize and use a lifetime which doesn’t share mutable configuration between grain types.

This example deliberately trades resource-aware balancing for affinity. If load is the primary concern, prefer ResourceOptimizedPlacement or apply a filter and let a built-in placement strategy choose from the remaining candidates. For more implementations, inspect the built-in directors under src/Orleans.Runtime/Placement, including HashBasedPlacementDirector.