HangfireIO/Hangfire · error · SqlServerDistributedLockException
Could not release a lock on the resource '{resource}': Serve
Error message
Could not release a lock on the resource '{resource}': Server returned the '{releaseResult}' error. What it means
SQL Server's sp_releaseapplock stored procedure returned a negative result code when Hangfire attempted to release a distributed lock. A negative return means the current session does not own the lock or it was already released. The exception type is SqlServerDistributedLockException.
Source
Thrown at src/Hangfire.SqlServer/SqlServerDistributedLock.cs:233
throw new SqlServerDistributedLockException(
$"Could not place a lock on the resource '{resource}': {(LockErrorMessages.TryGetValue(lockResult, out var message) ? message : $"Server returned the '{lockResult}' error.")}.");
}
} while (started.Elapsed < timeout);
throw new DistributedLockTimeoutException(resource);
}
internal static void Release(DbConnection connection, string resource)
{
using (var command = CreateReleaseCommand(connection, resource, out var resultParameter))
{
command.ExecuteNonQuery();
var releaseResult = (int)resultParameter.Value;
if (releaseResult < 0)
{
throw new SqlServerDistributedLockException(
$"Could not release a lock on the resource '{resource}': Server returned the '{releaseResult}' error.");
}
}
}
internal static DbCommand CreateReleaseCommand(
DbConnection connection,
string resource,
out DbParameter resultParameter)
{
return connection.Create("sp_releaseapplock", CommandType.StoredProcedure)
.AddParameter("@Resource", resource, DbType.String, size: 255)
.AddParameter("@LockOwner", LockOwner, DbType.String, size: 32)
.AddReturnParameter("@Result", out resultParameter, DbType.Int32);
}
}
}
View on GitHub (pinned to c236dd0f93)
Solutions
- Ensure the lock is acquired before releasing; never call Release without a successful prior acquire on the same connection/session.
- Use the using/dispose pattern on SqlServerDistributedLock rather than calling Release directly to guarantee correct acquire-release pairing.
- Enable connection resiliency ( SqlConnectionStringBuilder.ConnectRetryCount ) to survive transient session resets, or use Microsoft.Data.SqlClient which has better resiliency defaults.
Example fix
// before — manual acquire/release can get unpaired
SqlServerDistributedLock.Acquire(connection, "res", timeout);
// ... exception here ...
SqlServerDistributedLock.Release(connection, "res"); // release on session that may not hold it
// after — use the IDisposable wrapper for guaranteed pairing
using (new SqlServerDistributedLock(storage, "res", timeout))
{
DoWork();
} Defensive patterns
Strategy: try-catch
Try / catch
try
{
using (new SqlServerDistributedLock(storage, resource, timeout))
{
DoWork(); // Dispose handles release; no manual Release call needed
}
}
catch (SqlServerDistributedLockException ex) when (ex.Message.Contains("release"))
{
logger.Warning(ex, "Lock release returned an error for {Resource}; may be benign if session reset", resource);
} Prevention
- Always use the IDisposable SqlServerDistributedLock wrapper instead of calling Acquire/Release directly to guarantee correct pairing.
- Never call Release without a successful Acquire on the same connection and session.
- Be aware that SQL Azure may terminate idle connections (30 min), which can invalidate session-owned locks.
When it happens
Trigger: Calling Release on a lock resource that was never acquired in the current session; double-releasing a lock (the SqlServerDistributedLock.Dispose path already released it); the connection's session changed or was reset between acquire and release; a previous acquire failed but release is still attempted.
Common situations: Connection pooling returning a recycled SqlConnection whose session state differs from when the lock was acquired; exception in the using-block causing Dispose to run on a partially-initialized lock holder; SQL Azure connection-killing (idle termination) between acquire and release.
Related errors
- Could not place a lock on the resource '{resource}': {messag
- Timeout expired. The timeout elapsed prior to obtaining a di
- 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.
AI-assisted analysis of HangfireIO/Hangfire@c236dd0f93 (2026-08-13).
Data as JSON: /api/errors/14ce2f433ce31e36.
Report an issue: GitHub.