Issues with deadlocks when using the TryPublish in multiple threads simultaneously

Is anyone also having issues with deadlocks when publising multiple pages in thread simultaneously? I'm getting the following error:

CMS.EventLog.EventLogService[0]
Exception occured for object with ID: 19 of type: 9237e512-2430-436f-a06a-e31488aa7cec
Microsoft.Data.SqlClient.SqlException (0x80131904): Transaction (Process ID 59) was deadlocked on lock resources with another process and has been chosen as the deadlock victim. Rerun the transaction.
at Microsoft.Data.SqlClient.SqlConnection.OnError(SqlException exception, Boolean breakConnection, Action1 wrapCloseInAction) at Microsoft.Data.SqlClient.SqlInternalConnection.OnError(SqlException exception, Boolean breakConnection, Action1 wrapCloseInAction)
at Microsoft.Data.SqlClient.TdsParser.ThrowExceptionAndWarning(TdsParserStateObject stateObj, SqlCommand command, Boolean callerHasConnectionLock, Boolean asyncClose)
at Microsoft.Data.SqlClient.TdsParser.TryRun(RunBehavior runBehavior, SqlCommand cmdHandler, SqlDataReader dataStream, BulkCopySimpleResultSet bulkCopyHandler, TdsParserStateObject stateObj, Boolean& dataReady)
at Microsoft.Data.SqlClient.SqlDataReader.TryHasMoreRows(Boolean& moreRows)
at Microsoft.Data.SqlClient.SqlDataReader.TryReadInternal(Boolean setTimeout, Boolean& more)
at Microsoft.Data.SqlClient.SqlDataReader.Read()
at CMS.DataEngine.Internal.ExtendedDbDataReader.Read()
at CMS.DataEngine.DataQueryBase1.GetEnumerableResult(CommandBehavior commandBehavior, Boolean newConnection)+MoveNext() at CMS.DataEngine.DataExtensions.As[InfoType](IEnumerable1 dataRecords, Func2 createObjectFunc) at CMS.DataEngine.ObjectDependenciesRemover.GetDependencyData(RemoveDependencyWithApiSettings removeSettings, BaseInfo obj) at CMS.DataEngine.ObjectDependenciesRemover.RemoveDependencyUsingAPI(String objectType, RemoveDependencyWithApiSettings removeSettings) at CMS.DataEngine.ObjectDependenciesRemover.RemoveObjectDependenciesByAPI(IEnumerable1 queries)
at CMS.DataEngine.ObjectDependenciesRemover.RemoveObjectDependencies(ICollection1 dependencies, Boolean clearCache) at CMS.DataEngine.ObjectDependenciesRemover.RemoveObjectDependenciesAuto(BaseInfo infoObj, Boolean deleteAll, Boolean clearCache) at CMS.DataEngine.ObjectDependenciesRemover.RemoveObjectDependencies(BaseInfo infoObj, Boolean deleteAll, Boolean clearCache) at CMS.DataEngine.BaseInfo.RemoveObjectDependencies(Boolean deleteAll, Boolean clearCache) at CMS.DataEngine.AbstractInfoBase1.DeleteRelatedData()
at CMS.DataEngine.AbstractInfoBase1.DeleteDataSyncOrAsync(Boolean useAsync, CancellationToken cancellationToken) at CMS.DataEngine.AbstractInfoBase1.DeleteData()
at CMS.DataEngine.AbstractInfoBase1.GeneralizedInfoWrapper.DeleteData() at CMS.DataEngine.AbstractInfoProvider3.WithEvent[TBeforeEvent,TAfterEvent](Action action, IEnumerable1 beforeEventHandlers, TBeforeEvent beforeEvent, IEnumerable1 afterEventHandlers, Func1 afterEventFactory) at CMS.DataEngine.AbstractInfoProvider3.<>c__DisplayClass100_0.````b__0()
at CMS.DataEngine.SyncOrAsyncInvoker.InvokeSyncOrAsync(Boolean useAsync, Action syncAction, Func1 asyncAction) at CMS.DataEngine.AbstractInfoProvider3.DeleteInfoSyncOrAsync(Boolean useAsync, TInfo info, CancellationToken cancellationToken)
at CMS.DataEngine.AbstractInfoProvider3.DeleteInfo(TInfo info) at CMS.DataEngine.AbstractInfoProvider2.Delete(TInfo info)
at CMS.ContentEngine.Internal.ContentItemCommonDataInfo.DeleteObject()
at CMS.DataEngine.BaseInfo.Delete()
at CMS.ContentEngine.Core.ContentItemManagerCore.TryPublish(Int32 contentItemId, Int32 contentLanguageId, AfterPublishContentItemEventInitializer afterEventHandlerInitializer, CancellationToken cancellationToken)
at CMS.ContentEngine.ContentItemManager.TryPublish(Int32 contentItemId, String languageName, CancellationToken cancellationToken)

Answers

Can you please share the block of code that is involved and also your current version of xperience?

Accepted answer

Hey, I ran into the similar kind of issue and spent a good chunk of time tracing through the stack before I understood what was actually happening.

What's actually going on

The deadlock isn't in your code. It's happening deep inside XbyK's publish pipeline. 
When TryPublish runs, it first deletes the existing draft ContentItemCommonData record. 
Before the actual delete happens, ObjectDependenciesRemover.GetDependencyData fires a SELECT query inside an open transaction to figure out what related objects need cleaning up. When two threads hit that same step at the same time, even for completely different content items, they end up waiting on each other's transaction locks and SQL Server kills one of them as the deadlock.

My scenario — scheduled task with nested linked content items

I ran into a variant of this in a scheduled task that imports/updates content hub items where each parent item has several nested linked content items. The task was kicking off updates for everything in parallel, which meant TryPublish was being called simultaneously for the parent item and all its linked children.

This is actually worse than publishing unrelated items concurrently, because the parent and its linked items share overlapping dependency rows.

The method I was using for each update follows the documented XbyK pattern — TryCreateDraftTryUpdateDraftTryPublish — which is correct. The problem was purely in how the scheduled task was calling it.

What actually fixed it for me

Two things together:

First, publish in the right order nested linked items first, then the parent. XbyK doesn't cascade automatically via the API the way the UI does, so you need to sequence this yourself. Processing deepest children first means by the time you publish the parent, there are fewer unresolved dependency relationships in play.

Second, go fully sequential. No parallel foreach.

Just a plain foreach with await on each item:

Example what I used

**// nested/linked items first**
foreach (var linkedItemId in linkedItemIds)
    await UpdateContentItemByIdAsync(linkedItemId, ...);

**// parent last**
await UpdateContentItemByIdAsync(parentItemId, ...);

/// Example reference code block
public async Task UpdateContentItemByIdAsync(
    int contentItemId,
    string contentType,
    Dictionary<string, object> updatedFieldData,
    string languageName,
    CancellationToken cancellationToken)
{
    try
    {
        var user = userInfoProvider.Get("administrator");
        var contentItemManager = contentItemManagerFactory.Create(user.UserID);
        var updatedData = new ContentItemData(updatedFieldData);

        // Add detailed logging before attempting update
        logger.LogInformation(
            new EventId(0, "UPDATE_ATTEMPT"),
            "Attempting to update content item ID {ItemId} of type '{ContentType}' in language '{Language}'",
            contentItemId, contentType, languageName);

        // Try to create draft first (in case no draft exists)
        logger.LogDebug(
            new EventId(0, "CREATE_DRAFT_ATTEMPT"),
            "Creating draft for content item ID {ItemId}",
            contentItemId);

        await contentItemManager.TryCreateDraft(contentItemId, languageName, cancellationToken);

        // Check the result of TryUpdateDraft
        bool updateResult = await contentItemManager.TryUpdateDraft(
            contentItemId,
            languageName,
            updatedData,
            cancellationToken);

        if (!updateResult)
        {
            // Log detailed error when update fails
            logger.LogError(
                new EventId(0, "UPDATE_DRAFT_FAILED"),
                "TryUpdateDraft returned FALSE for content item ID {ItemId}, language '{Language}', type '{ContentType}'. " +
                "Possible causes: no draft exists, language variant missing, or invalid content item ID.",
                contentItemId, languageName, contentType);
            return; // Exit early - don't try to publish if update failed
        }

        logger.LogInformation(
            new EventId(0, "UPDATE_DRAFT_SUCCESS"),
            "Successfully updated draft for content item ID {ItemId}",
            contentItemId);

        // Check the result of TryPublish
        bool publishResult = await contentItemManager.TryPublish(
            contentItemId,
            languageName,
            cancellationToken);

        if (!publishResult)
        {
            //  Log detailed error when publish fails
            logger.LogError(
                new EventId(0, "PUBLISH_FAILED"),
                "TryPublish returned FALSE for content item ID {ItemId}, language '{Language}'. " +
                "Draft was updated but not published.",
                contentItemId, languageName);
            return; // Exit early
        }

        //  Enhanced success logging
        logger.LogInformation(
            new EventId(0, "UPDATE_SUCCESS"),
            "Successfully updated and published content item ID {ItemId} of type '{ContentType}'",
            contentItemId, contentType);

        // Invalidate cache for this content item
        CacheHelper.TouchKey($"contentitem|{contentItemId}");
        CacheHelper.TouchKey($"contenttype|{contentType}");
        
        // Also invalidate any cached items by GUID if we can retrieve it
        // This ensures the cache keys used in MappedContentItemRepository are invalidated
        try
        {
            var queryBuilder = new ContentItemQueryBuilder()
                .ForContentType(contentType, parameters =>
                {
                    parameters.TopN(1)
                        .Where(w => w.WhereEquals("ContentItemID", contentItemId))
                        .Columns(nameof(IContentItemFieldsSource.SystemFields.ContentItemGUID));
                });

            var guids = await contentQueryExecutor.GetResult(
                queryBuilder,
                rowData => rowData.ContentItemGUID,
                new ContentQueryExecutionOptions { ForPreview = false },
                cancellationToken);

            var guid = guids.FirstOrDefault();
            if (guid != Guid.Empty)
            {
                // Invalidate cache keys used in MappedContentItemRepository
                // Format: "getcontentitem|{ContentItemGuid}|{lang}"
                // We'll touch the content item cache key which should invalidate dependent caches
                CacheHelper.TouchKey($"contentitem|{contentItemId}");
            }
        }
        catch (Exception cacheEx)
        {
            // Log but don't fail the update if cache invalidation fails
            logger.LogWarning(
                new EventId(0, "CACHE_INVALIDATION_WARNING"),
                cacheEx,
                "Failed to invalidate cache for content item ID {ItemId}, but update was successful",
                contentItemId);
        }
    }
    catch (Exception ex)
    {
        logger.LogError(
            new EventId(0, "UPDATE_ERROR"),
            ex,
            "Exception while updating content item ID {ItemId} of type '{ContentType}'",
            contentItemId, contentType);
        throw;
    }
}

Thanks
Nikhila

Check, thanks, that was what I already feared. I also solved it by doing the 'TryPublish' only once at a time.

To response this discussion, you have to login first.