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

  1. Ensure OnPerforming completed successfully (lock acquired) before OnPerformed runs — investigate any storage errors during AcquireDistributedLock.
  2. Do not clear or modify context.Items in custom IServerFilter implementations that run alongside DisableConcurrentExecutionAttribute.
  3. 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

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


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