HangfireIO/Hangfire · error · SqlServerDistributedLockException

Could not release a lock on the resource '{lockCommand.Item3

Error message

Could not release a lock on the resource '{lockCommand.Item3}': Server returned the '{releaseResult}' error.

What it means

SqlServerDistributedLockException thrown inside SqlServerWriteOnlyTransaction.Commit (SqlServerWriteOnlyTransaction.cs:104) after the command batch executes. For each lock acquired via AcquireDistributedLock, a release command (sp_releaseapplock) runs within the batch; its output parameter is read back and, if the value is negative (< 0), this exception is thrown. Negative sp_releaseapplock return codes indicate the session does not own the lock, the lock resource does not exist, or the lock was already released/rolled back by the engine.

Source

Thrown at src/Hangfire.SqlServer/SqlServerWriteOnlyTransaction.cs:104

                        {
                            commandBatch.Append(command.Item1);
                        }

                        commandBatch.CommandTimeout = storage.CommandTimeout;
                        commandBatch.CommandBatchMaxTimeout = storage.CommandBatchMaxTimeout;

                        commandBatch.ExecuteNonQuery();
                        foreach (var acquiredLock in ctx._acquiredLocks)
                        {
                            acquiredLock.TryReportReleased();
                        }

                        foreach (var lockCommand in ctx._lockCommands)
                        {
                            var releaseResult = lockCommand.Item2.GetParameterValue<int?>();
                            if (releaseResult.HasValue && releaseResult.Value < 0)
                            {
                                throw new SqlServerDistributedLockException($"Could not release a lock on the resource '{lockCommand.Item3}': Server returned the '{releaseResult}' error.");
                            }
                        }
                        
                        foreach (var queueCommand in ctx._queueCommandQueue)
                        {
                            queueCommand(connection, transaction);
                        }
                    }
                }, this);

                _committed = true;
            }
            finally
            {
                foreach (var acquiredLock in _acquiredLocks)
                {
                    acquiredLock.Dispose();
                }

View on GitHub (pinned to c236dd0f93)

Solutions

  1. Inspect the negative return code in the message (e.g. -999 'lock not found', -1 'timeout') and the SQL Server error log to identify why ownership was lost.
  2. Prevent connection resets from clearing applock session state (avoid Connection Reset=true resets mid-transaction; keep acquire and release on the same physical connection, as the dedicated connection already does).
  3. Shorten the transaction / reduce the number of commands in the batch so the applock does not exceed its lifetime or the batch does not error mid-way.
  4. Ensure xact_abort and the batch do not error before the release command; fix the underlying SQL error that causes the engine to roll back and auto-release the applock.
  5. Confirm the resource name passed to AcquireDistributedLock exactly matches what is released (the dedicated connection path handles this, so a mismatch points to a custom override).
Defensive patterns

Strategy: try-catch

Try / catch

try
{
    transaction.Commit();
}
catch (SqlServerDistributedLockException ex)
{
    // Log the negative sp_releaseapplock code in ex.Message; ownership was lost before release.
    // Treat as transient: the batch itself may have committed/rolled back; reconcile job state
    // and decide whether to retry or surface the failure.
    logger.ErrorException("Distributed lock release failed during commit.", ex);
    throw;
}

Prevention

When it happens

Trigger: A WriteOnlyTransaction that called AcquireDistributedLock commits, and the embedded sp_releaseapplock returns a negative code. This occurs when lock ownership was lost before release: the connection was reset (clearing the session that held the applock), xact_abort or a mid-batch error already rolled the transaction back (releasing applocks automatically), the applock timed out, or the resource/session-scope differs between acquire and release.

Common situations: Connection pooling with aggressive resets stripping session-scoped applocks; long transactions exceeding the applock lifetime; SQL Server deadlocks/errors that abort the batch so the engine releases the applock before the explicit release runs; running under distributed transactions where the transaction is enlisted/disposed differently; concurrent workers contending on the same resource.

Related errors


AI-assisted analysis of HangfireIO/Hangfire@c236dd0f93 (2026-08-13). Data as JSON: /api/errors/cbed3e57f2a442de. Report an issue: GitHub.