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
- 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.
- 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).
- 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.
- 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.
- 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
- Keep transactions short so applocks do not outlive their lifetime or hit errors mid-batch.
- Avoid connection resets that clear session-scoped applock state mid-transaction; reuse the dedicated connection.
- Monitor the SQL Server error log for the negative return codes to find the root cause (lock not found, timeout, etc.).
- Fix any underlying SQL errors that cause xact_abort to roll back the batch before the release command runs.
- Track the resource name used in AcquireDistributedLock to ensure acquire and release stay consistent.
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
- Timeout argument value should be greater than zero.
- Can not release a distributed lock: it was not acquired.
- Connection must be open before acquiring a distributed lock.
- Unable to obtain resource identifier: {ex.Message}
- Timeout value should be equal to or greater than zero.
AI-assisted analysis of HangfireIO/Hangfire@c236dd0f93 (2026-08-13).
Data as JSON: /api/errors/cbed3e57f2a442de.
Report an issue: GitHub.