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

  1. Catch TableStorageUpdateConditionNotSatisfiedException and re-read state then retry the write (the grain runtime does this automatically up to a configured limit — check MaxStorageErrorsBeforeReenter).
  2. Ensure only one activation of each grain is active at a time (single-activation guarantee — check that placement and activation are correct).
  3. Reduce write contention by designing grains to avoid concurrent writes to the same entity.
  4. 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

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


AI-assisted analysis of dotnet/orleans@fca799fa70 (2026-08-13). Data as JSON: /api/errors/a3bfab2344bf64b3. Report an issue: GitHub.