HangfireIO/Hangfire · error · InvalidOperationException
Can not release a distributed lock: it was not acquired.
Error message
Can not release a distributed lock: it was not acquired.
What it means
DisableConcurrentExecutionAttribute.OnPerformed (DisableConcurrentExecutionAttribute.cs:59) attempts to release the distributed lock acquired in OnPerforming by reading it from context.Items["DistributedLock"]. If the key is absent the lock was never acquired (OnPerforming threw before storing it, or a custom filter removed it), so releasing is impossible and an InvalidOperationException is thrown to signal state corruption.
Source
Thrown at src/Hangfire.Core/DisableConcurrentExecutionAttribute.cs:59
[CanBeNull]
public string Resource { get; }
public int TimeoutSec { get; }
public void OnPerforming(PerformingContext context)
{
var resource = GetResource(context.BackgroundJob.Job);
var timeout = TimeSpan.FromSeconds(TimeoutSec);
var distributedLock = context.Connection.AcquireDistributedLock(resource, timeout);
context.Items["DistributedLock"] = distributedLock;
}
public void OnPerformed(PerformedContext context)
{
if (!context.Items.TryGetValue("DistributedLock", out var value))
{
throw new InvalidOperationException("Can not release a distributed lock: it was not acquired.");
}
var distributedLock = (IDisposable)value;
distributedLock.Dispose();
}
private string GetResource(Job job)
{
if (!String.IsNullOrWhiteSpace(Resource))
{
try
{
return String.Format(CultureInfo.InvariantCulture, Resource, job.Args.ToArray()).ToLowerInvariant();
}
catch (Exception ex)
{
throw new FormatException($"Unable to obtain resource identifier: {ex.Message}");
}View on GitHub (pinned to c236dd0f93)
Solutions
- Ensure OnPerforming completed successfully (lock acquired) before OnPerformed runs — investigate any storage errors during AcquireDistributedLock.
- Do not clear or modify context.Items in custom IServerFilter implementations that run alongside DisableConcurrentExecutionAttribute.
- If writing tests, simulate the full OnPerforming -> OnPerformed lifecycle rather than calling OnPerformed in isolation.
Example fix
// before: test calls OnPerformed directly without OnPerforming filter.OnPerformed(performedContext); // after: call OnPerforming first so the lock item is set filter.OnPerforming(performingContext); // ... job runs ... filter.OnPerformed(performedContext);
Defensive patterns
Strategy: validation
Validate before calling
// In custom filters that interleave with DisableConcurrentExecution,
// never remove context.Items entries you did not add.
// Before OnPerformed, verify the lock item exists:
if (!context.Items.ContainsKey("DistributedLock"))
throw new InvalidOperationException(
"DistributedLock missing — OnPerforming may have failed."); Try / catch
// OnPerformed is called by the pipeline; if it throws,
// catch in an outer IServerExceptionFilter:
public void OnServerException(ServerExceptionContext ctx)
{
if (ctx.Exception is InvalidOperationException ex
&& ex.Message.Contains("not acquired"))
{
// log and suppress — lock acquisition failed upstream
ctx.ExceptionHandled = true;
}
} Prevention
- Ensure OnPerforming succeeds (lock acquired) before OnPerformed runs — investigate storage errors.
- Never clear or modify context.Items in custom IServerFilter implementations.
- In unit tests, always simulate the full OnPerforming -> OnPerformed lifecycle.
When it happens
Trigger: The OnPerformed handler runs but the "DistributedLock" item is missing from context.Items. This happens if OnPerforming threw an exception before or during AcquireDistributedLock (so the item was never set), or if another filter in the pipeline cleared context.Items.
Common situations: The distributed lock acquisition itself threw (storage connectivity issue, timeout) but OnPerformed still fires due to partial pipeline execution; a misbehaving custom filter that manipulates context.Items; unit testing the filter without a real OnPerforming call.
Related errors
- Timeout argument value should be greater than zero.
- Could not release a lock on the resource '{lockCommand.Item3
- Unable to obtain resource identifier: {ex.Message}
- Connection must be open before acquiring a distributed lock.
- Could not place a lock on the resource '{resource}': {messag
AI-assisted analysis of HangfireIO/Hangfire@c236dd0f93 (2026-08-13).
Data as JSON: /api/errors/65859f8b0917f866.
Report an issue: GitHub.