HangfireIO/Hangfire · error · InvalidOperationException

Connection must be open before acquiring a distributed lock.

Error message

Connection must be open before acquiring a distributed lock.

What it means

InvalidOperationException thrown by SqlServerDistributedLock.Acquire when the supplied DbConnection is not in the Open state. Hangfire acquires an application lock (sp_getapplock) on the connection and must keep it for the lock's lifetime; Dapper would auto-close a closed connection after the command and release the lock immediately, so Hangfire refuses to run on a closed connection.

Source

Thrown at src/Hangfire.SqlServer/SqlServerDistributedLock.cs:176

                    // OBSOLETE. This class is not used anymore by the SqlServerConnection
                    // class. The problem above was solved there by establishing a
                    // dedicated connection, when there is at least one acquired lock.
                    // Since the acquisition, all the commands and transactions are routed
                    // through that connection to ensure all the locks are still active.
                }
            }
        }

        internal static void Acquire(DbConnection connection, string resource, TimeSpan timeout)
        {
            if (connection.State != ConnectionState.Open)
            {
                // When we are passing a closed connection to Dapper's Execute method,
                // it kindly opens it for us, but after command execution, it will be closed
                // automatically, and our just-acquired application lock will immediately
                // be released. This is not behavior we want to achieve, so let's throw an
                // exception instead.
                throw new InvalidOperationException("Connection must be open before acquiring a distributed lock.");
            }

            var started = Stopwatch.StartNew();

            // We can't pass our timeout directly to the sp_getapplock stored procedure, because
            // high values, such as minute or more, may cause SQL Server's thread pool starvation,
            // when the number of connections that try to acquire a lock is more than the number of 
            // available threads in SQL Server. In this case a deadlock will occur, when SQL Server 
            // tries to schedule some more work for a connection that acquired a lock, but all the 
            // available threads in a pool waiting for that lock to be released.
            //
            // So we are trying to acquire a lock multiple times instead, with timeout that's equal
            // to seconds, not minutes.
            var lockTimeout = (long) Math.Min(LockTimeout.TotalMilliseconds, timeout.TotalMilliseconds);

            do
            {
                using var command = connection

View on GitHub (pinned to c236dd0f93)

Solutions

  1. Open the connection explicitly before acquiring the distributed lock.
  2. Ensure the connection stays open for the entire duration the lock is held, then dispose it.
  3. Acquire connections from SqlServerStorage's UseConnection helper when possible so open/close is managed for you.

Example fix

// before
using var conn = new SqlConnection(cs);
SqlServerDistributedLock.Acquire(conn, resource, timeout); // conn closed -> throws

// after
using var conn = new SqlConnection(cs);
conn.Open();
SqlServerDistributedLock.Acquire(conn, resource, timeout);
Defensive patterns

Strategy: validation

Validate before calling

static DbConnection EnsureOpen(DbConnection conn)
{
    if (conn.State != ConnectionState.Open) conn.Open();
    return conn;
}

Type guard

static bool IsConnectionOpen(DbConnection conn) => conn.State == ConnectionState.Open;

Prevention

When it happens

Trigger: Calling SqlServerDistributedLock.Acquire (directly or via a distributed lock acquisition path) with a DbConnection whose State != Open.

Common situations: Forgetting connection.Open() before acquiring a lock; passing a connection from a scope/pool that closes between uses; relying on Dapper's auto-open behavior; an exception path that left a connection closed.

Related errors


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