# Distributed Locking in .NET: Coordinating Work Across Multiple Instances

> Learn how to coordinate work across multiple application instances with distributed locking in .NET, preventing race conditions in scaled-out systems. Explore two approaches: PostgreSQL advisory locks and the DistributedLock library.

Published: 2025-09-20. Author: Milan Jovanović.

Canonical: https://milanjovanovic.tech/blog/distributed-locking-in-dotnet-coordinating-work-across-multiple-instances

Distributed locking lets one application instance hold a critical section while the others wait, which in-process primitives like `lock` and `SemaphoreSlim` cannot do once you scale out.
In .NET you can build it on PostgreSQL advisory locks with `pg_try_advisory_lock`, or use the DistributedLock library with a Postgres, Redis, or SQL Server backend.

When you build applications that run across multiple servers or processes, you eventually run into the problem of concurrent access.
Multiple workers try to update the same resource at the same time, and you end up with race conditions, duplicated work, or corrupted data.

.NET provides excellent [**concurrency control primitives**](https://milanjovanovic.tech/blog/introduction-to-locking-and-concurrency-control-in-dotnet-6) for single-process scenarios,
like `lock`, `SemaphoreSlim`, and `Mutex`.
But when your application is scaled out across multiple instances, these primitives don't work anymore.

That's where **distributed locking** comes in.

Distributed locking provides a solution by ensuring **only one node** (application instance) can access a critical section at a time,
preventing race conditions and maintaining data consistency **across your distributed system**.

## Why and When You Need Distributed Locking

In a single-process app, you can just use `lock` or the new [Lock class](https://learn.microsoft.com/en-us/dotnet/api/system.threading.lock) in .NET 10.
But once you scale out, that's not enough, because each process has its own memory space.

A few common cases where distributed locks are valuable:

- **Background jobs**: ensuring only one worker processes a particular job or resource at a time.
- **Leader election**: choosing a single process to perform periodic work (like applying async database projections).
- **Avoiding double execution**: ensuring scheduled tasks don't run multiple times when deployed to multiple instances.
- **Coordinating shared resources**: e.g., only one service instance performing a migration or cleanup at a time.
- **Cache stampede prevention**: ensuring only one instance refreshes the cache when a given cache key expires.

The key value: consistency and safety across distributed environments.
Without this, you risk duplicate operations, corrupted state, or unnecessary load.

Now you know why distributed locking is important.

Let's look at some implementation options.

## DIY Distributed Locking with PostgreSQL Advisory Locks

Let's start simple.
PostgreSQL has a feature called [advisory locks](https://www.postgresql.org/docs/current/explicit-locking.html#ADVISORY-LOCKS) that's perfect for distributed locking.
Unlike table locks, these don't interfere with your data - they're purely for coordination.

Here's an example:

```csharp {10,25}
public class NightlyReportService(NpgsqlDataSource dataSource)
{
    public async Task ProcessNightlyReport()
    {
        await using var connection = dataSource.OpenConnection();

        var key = HashKey("nightly-report");

        var acquired = await connection.ExecuteScalarAsync<bool>(
            "SELECT pg_try_advisory_lock(@key)",
            new { key });

        if (!acquired)
        {
            throw new ConflictException("Another instance is already processing the nightly report");
        }

        try
        {
            await DoWork();
        }
        finally
        {
            await connection.ExecuteAsync(
                "SELECT pg_advisory_unlock(@key)",
                new { key });
        }
    }

    private static long HashKey(string key) =>
        BitConverter.ToInt64(SHA256.HashData(Encoding.UTF8.GetBytes(key)), 0);

    private static Task DoWork() => Task.Delay(5000); // Your actual work here
}
```

Here's what's happening under the hood.

First, we convert our lock name into a number.
PostgreSQL **advisory locks need numeric keys**, so we hash `nightly-report` into a 64-bit integer.
Every node (application instance) must generate the same number for the same string, or this won't work.

Next, `pg_try_advisory_lock()` attempts to grab an exclusive lock on that number.
It returns `true` if successful, `false` if another connection already holds it.
This call doesn't block - it tells you immediately whether you got the lock.

If we get the lock, we do our work.
If not, we return a conflict response and let the other instance handle it.

The `finally` block ensures we always release the lock, even if something goes wrong.
PostgreSQL also **automatically releases advisory locks when connections close**, which is a nice safety net.

SQL Server has a similar feature with [sp_getapplock](https://learn.microsoft.com/en-us/sql/relational-databases/system-stored-procedures/sp-getapplock-transact-sql).

## Exploring the DistributedLock Library

While the DIY approach works, production applications need more sophisticated features.
The [DistributedLock](https://github.com/madelson/DistributedLock) library handles the edge cases and
provides multiple backend options (Postgres, Redis, SqlServer, etc.).
You know I'm a fan of not reinventing the wheel, so this is a great choice.

Install the package:

```powershell
Install-Package DistributedLock
```

I'll use the approach with `IDistributedLockProvider` which works nicely with DI.
You can acquire a lock without having to know anything about the underlying infrastructure.
All you have to do is register a lock provider implementation in your DI container.

For example, using Postgres:

```csharp
// Register the distributed lock provider
builder.Services.AddSingleton<IDistributedLockProvider>(
    (_) =>
    {
        return new PostgresDistributedSynchronizationProvider(
            builder.Configuration.GetConnectionString("distributed-locking")!);
    });
```

Or if you want to use Redis with the [Redlock algorithm](https://redis.io/docs/latest/develop/clients/patterns/distributed-locks/):

```csharp
// Requires StackExchange.Redis
builder.Services.AddSingleton<IConnectionMultiplexer>(
    (_) =>
    {
        return ConnectionMultiplexer.Connect(
            builder.Configuration.GetConnectionString("redis")!);
    });

// Register the distributed lock provider
builder.Services.AddSingleton<IDistributedLockProvider>(
    (sp) =>
    {
        var connectionMultiplexer = sp.GetRequiredService<IConnectionMultiplexer>();

        return new RedisDistributedSynchronizationProvider(connectionMultiplexer.GetDatabase());
    });
```

The usage is straightforward:

```csharp {4}
// You can also pass in a timeout, where the provider will keep retrying to acquire the lock
// until the timeout is reached.
IDistributedSynchronizationHandle? distributedLock = distributedLockProvider
    .TryAcquireLock("nightly-report");

// If we didn't get the lock, the object will be null
if (distributedLock is null)
{
    return Results.Conflict();
}

// It's important to wrap the lock in a using statement to ensure it's released properly
using (distributedLock)
{
    await DoWork();
}
```

The library handles all the tricky parts: timeouts, retries, and ensuring locks are released even in failure scenarios.

It also supports many backends (SQL Server, Azure, ZooKeeper, etc.), making it a solid choice for production workloads.

## Wrapping Up

**Distributed locking** isn't something you need every day.
But when you do, it saves you from subtle, painful bugs that only appear under load or in production.

**Start simple**: if you're already using Postgres, **advisory locks** are a powerful tool.
I now run that exact pattern in production, and the full implementation (blocking acquire, key hashing, the fail-open decision) is in [**distributed locking with Postgres advisory locks**](https://milanjovanovic.tech/blog/postgres-advisory-locks-dotnet).

For a cleaner developer experience, reach for the **DistributedLock library**.

Choose the backend that fits your infrastructure (Postgres, Redis, SQL Server, etc.).

The right lock at the right time ensures your system stays consistent, reliable, and resilient, even across multiple processes and servers.

---

## Frequently asked questions

### What is distributed locking?

Distributed locking ensures only one node (application instance) can access a critical section at a time across a distributed system. It prevents race conditions, duplicate operations, and corrupted state when an application is scaled out across multiple servers or processes.

### Why doesn't the C# lock statement work across multiple instances?

In-process primitives like lock, SemaphoreSlim, and Mutex only coordinate threads within a single process. Once an application is scaled out, each instance has its own memory space, so those primitives cannot see each other and you need a shared external lock instead.

### When do you need a distributed lock?

Common cases: making sure only one worker processes a background job or resource, leader election for periodic work, preventing scheduled tasks from double-executing across instances, coordinating one-at-a-time work like migrations or cleanup, and preventing cache stampedes when a cache key expires.

### How do PostgreSQL advisory locks work for distributed locking?

Advisory locks are purely for coordination and never touch table data. You hash the lock name into a 64-bit integer, then call SELECT pg_try_advisory_lock(key), which immediately returns true or false without blocking. PostgreSQL also releases advisory locks automatically when the connection closes.

### Should you use the DistributedLock library or build your own?

For production, the DistributedLock library is a solid choice: it handles timeouts, retries, and releasing locks in failure scenarios, and supports Postgres, Redis, SQL Server, Azure, and ZooKeeper backends through an IDistributedLockProvider you register in DI. DIY advisory locks work well if you already run Postgres.

### Does SQL Server have an equivalent of Postgres advisory locks?

Yes. SQL Server offers sp_getapplock, a system stored procedure that provides application-level locks similar to PostgreSQL advisory locks, so you can coordinate work across instances without locking your actual data.
