{"record":{"id":"cbed3e57f2a442de","repo":"HangfireIO/Hangfire","slug":"could-not-release-a-lock-on-the-resource-lockcom","errorCode":null,"errorMessage":"Could not release a lock on the resource '{lockCommand.Item3}': Server returned the '{releaseResult}' error.","messagePattern":"Could not release a lock on the resource '(.+?)': Server returned the '(.+?)' error\\.","errorType":"exception","errorClass":"SqlServerDistributedLockException","httpStatus":null,"severity":"error","filePath":"src/Hangfire.SqlServer/SqlServerWriteOnlyTransaction.cs","lineNumber":104,"sourceCode":"                        {\n                            commandBatch.Append(command.Item1);\n                        }\n\n                        commandBatch.CommandTimeout = storage.CommandTimeout;\n                        commandBatch.CommandBatchMaxTimeout = storage.CommandBatchMaxTimeout;\n\n                        commandBatch.ExecuteNonQuery();\n                        foreach (var acquiredLock in ctx._acquiredLocks)\n                        {\n                            acquiredLock.TryReportReleased();\n                        }\n\n                        foreach (var lockCommand in ctx._lockCommands)\n                        {\n                            var releaseResult = lockCommand.Item2.GetParameterValue<int?>();\n                            if (releaseResult.HasValue && releaseResult.Value < 0)\n                            {\n                                throw new SqlServerDistributedLockException($\"Could not release a lock on the resource '{lockCommand.Item3}': Server returned the '{releaseResult}' error.\");\n                            }\n                        }\n                        \n                        foreach (var queueCommand in ctx._queueCommandQueue)\n                        {\n                            queueCommand(connection, transaction);\n                        }\n                    }\n                }, this);\n\n                _committed = true;\n            }\n            finally\n            {\n                foreach (var acquiredLock in _acquiredLocks)\n                {\n                    acquiredLock.Dispose();\n                }","sourceCodeStart":86,"sourceCodeEnd":122,"githubUrl":"https://github.com/HangfireIO/Hangfire/blob/c236dd0f930f831ec151e436e138ddc429a02a72/src/Hangfire.SqlServer/SqlServerWriteOnlyTransaction.cs#L86-L122","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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)."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try\n{\n    transaction.Commit();\n}\ncatch (SqlServerDistributedLockException ex)\n{\n    // Log the negative sp_releaseapplock code in ex.Message; ownership was lost before release.\n    // Treat as transient: the batch itself may have committed/rolled back; reconcile job state\n    // and decide whether to retry or surface the failure.\n    logger.ErrorException(\"Distributed lock release failed during commit.\", ex);\n    throw;\n}","preventionTips":["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."],"tags":["distributed-lock","sqlserver","transaction","concurrency","applock"],"backgroundTag":null,"analyzedSha":"c236dd0f930f831ec151e436e138ddc429a02a72","analyzedAt":"2026-08-13T20:27:11.027Z","schemaVersion":2},"datasetVersion":"2026-08-14T00:17:13.853Z"}