dotnet/orleans · error · TableStorageUpdateConditionNotSatisfiedException
Table storage condition not Satisfied. GrainType: {0}, Grai
Error message
Table storage condition not Satisfied. GrainType: {0}, GrainId: {1}, TableName: {2}, StoredETag: {3}, CurrentETag: {4} What it means
Thrown indirectly by DoOptimisticUpdate when Azure Table returns RequestFailedException with PreconditionFailed, Conflict, or NotFound status. This is an optimistic concurrency control (ETag) violation: the stored ETag does not match the ETag the client expected, meaning another writer modified or deleted the entity between the read and the write. Wrapped into TableStorageUpdateConditionNotSatisfiedException.
Source
Thrown at src/Azure/Orleans.Persistence.AzureStorage/Providers/Storage/AzureTableStorage.cs:170
grainState.RecordExists = false;
grainState.State = CreateInstance<T>();
}
catch (Exception exc)
{
LogErrorClearingGrainState(operation, grainType, grainId, grainState.ETag!, this.options.TableName, exc);
throw;
}
}
private static async Task DoOptimisticUpdate(Func<Task> updateOperation, string grainType, GrainId grainId, string tableName, string? currentETag)
{
try
{
await updateOperation.Invoke().ConfigureAwait(false);
}
catch (RequestFailedException ex) when (ex.IsPreconditionFailed() || ex.IsConflict() || ex.IsNotFound())
{
throw new TableStorageUpdateConditionNotSatisfiedException(grainType, grainId.ToString(), tableName, "Unknown", currentETag, ex);
}
}
/// <summary>
/// Serialize to Azure storage format in either binary or JSON format.
/// </summary>
/// <param name="grainState">The grain state data to be serialized</param>
/// <param name="entity">The Azure table entity the data should be stored in</param>
/// <remarks>
/// See:
/// http://msdn.microsoft.com/en-us/library/system.web.script.serialization.javascriptserializer.aspx
/// for more on the JSON serializer.
/// </remarks>
internal void ConvertToStorageFormat<T>(T grainState, TableEntity entity)
{
var binaryData = storageSerializer.Serialize<T>(grainState);
CheckMaxDataSize(binaryData.ToMemory().Length, MAX_DATA_CHUNK_SIZE * MAX_DATA_CHUNKS_COUNT);View on GitHub (pinned to fca799fa70)
Solutions
- Catch TableStorageUpdateConditionNotSatisfiedException and re-read state then retry the write (the grain runtime does this automatically up to a configured limit — check MaxStorageErrorsBeforeReenter).
- Ensure only one activation of each grain is active at a time (single-activation guarantee — check that placement and activation are correct).
- Reduce write contention by designing grains to avoid concurrent writes to the same entity.
- Use a higher retry count or exponential backoff for transient ETag conflicts.
Defensive patterns
Strategy: retry
Try / catch
try { await grain.WriteStateAsync(); }
catch (TableStorageUpdateConditionNotSatisfiedException ex)
{
logger.LogWarning(ex, "ETag conflict on {GrainType} {GrainId} — re-reading state.", ex.GrainType, ex.GrainId);
// Re-read and retry — Orleans runtime typically handles this internally,
// but for manual storage calls you must retry explicitly.
await grain.ReadStateAsync();
await grain.WriteStateAsync();
} Prevention
- Ensure single-activation per grain ID to avoid concurrent writers.
- Design grain logic to avoid concurrent state modifications from multiple callers.
- Let the Orleans runtime handle ETag retries via its built-in storage retry mechanism.
- Monitor for high ETag conflict rates as a signal of contention or split activations.
When it happens
Trigger: A grain reads state (gets ETag A), another activation or process writes the same entity (changing ETag to B), then the first grain tries to write with the stale ETag A. Azure Table rejects the replace/merge with HTTP 412 (PreconditionFailed) or 409 (Conflict) or 404 (NotFound if the entity was deleted).
Common situations: Concurrent grain activations for the same grain ID across silos. Reentrant grains that modify shared state. A delayed retry that writes after another write has already committed. Entity deleted by a cleanup process between read and write. Race condition in multi-silo deployments.
Related errors
- Cosmos DB condition not satisfied. GrainType: {0}, GrainId:
- Blob storage condition not Satisfied. BlobName: {blob.Name},
- GrainState-Table property not initialized
- Data too large to write to Azure table. Size={0} MaxSize={1}
- Unable to convert from storage format GrainStateEntity.Data=
AI-assisted analysis of dotnet/orleans@fca799fa70 (2026-08-13).
Data as JSON: /api/errors/a3bfab2344bf64b3.
Report an issue: GitHub.