# Fencing Tokens in .NET

Distributed-lock examples usually finish once one application instance has acquired a lease and every other instance has been refused. The code enters a `try` block, performs the work and releases the lock in `finally`. That handles the normal path. It doesnt handle a process that pauses for longer than its lease, loses ownership and then resumes without realising that another process has taken over.

At that point, both processes can act as though they own the resource. Extending the lease reduces how often this happens, but it cannot prove that a worker still owns the lock at the moment it changes the protected resource. A fencing token closes that gap by giving every new lock owner a monotonically increasing generation number. The resource receiving the write uses that number to reject stale owners.

This article builds that protection in .NET using SQL Server, then looks at the design decisions that determine whether it works in production.

## Where an ordinary lease fails

Suppose two worker instances generate the same monthly account statement. A distributed lock ensures that only one worker should run the generation process at a time. Worker A acquires the lock for 30 seconds and starts producing the statement. It then experiences a long garbage collection pause, host suspension, network partition or thread pool starvation. From the lock service's perspective, Worker A has stopped renewing its lease.

After 30 seconds, the lease expires. Worker B acquires the same lock and produces the statement successfully. Worker A then resumes. Unless the shared resource performs another ownership check, Worker A can overwrite Worker B's result with older data.

![](https://cdn.hashnode.com/uploads/covers/67c36038c69a4b7143c5fc49/5c4bf703-cef7-4e38-82e0-3e341573ecb6.png align="center")

Nothing in this sequence requires a broken lock implementation. The lock service behaved correctly, it allowed Worker B to acquire an expired lease. Worker A was also behaving normally from its own limited perspective. A paused process does not execute renewal callbacks, cancellation callbacks or `finally` blocks. When it resumes, the process continues from the instruction at which it stopped.

The same failure can happen without a process pause. A request may leave Worker A while its lease is valid, spend 40 seconds in a network queue and reach the destination after Worker B's later request. Checking the lock immediately before sending the request does not help because ownership can change between the check and the side effect.

## What the fencing token adds

A fencing token is an integer issued whenever ownership passes to a new lock holder. If Worker A receives token `41`, the next owner receives `42`, followed by `43`, and so on. Gaps are harmless. The ordering is the useful property. Every operation against the protected resource carries the token. The resource stores the highest token it has accepted and rejects operations carrying a lower value. Once token `42` has been observed, token `41` can never change that resource again.

The token therefore has to cross the full critical section. Generating it in the locking component and then dropping it before the final write provides no protection.

A usable fencing design has three properties:

1.  A new ownership generation receives a token greater than every earlier generation for that resource.
    
2.  Acquisition of the lease and allocation of its token happen as one linearised operation.
    
3.  The protected resource compares and records the token atomically with the write.
    

The third property is the one most often missed. Code such as `GetLastTokenAsync()` followed by `SaveAsync()` has another race between the check and the save. The comparison belongs in the database `UPDATE`, storage precondition or service endpoint that applies the change. Official Redis guidance now explicitly recommends fencing tokens for distributed locks, particularly where work can take a significant amount of time. Hazelcast's `FencedLock` documentation describes the same long pause scenario and issues a monotonically increasing token when ownership changes.

## The token is an ownership generation

The token should be treated as an opaque ownership generation, even if its implementation is a `long`. Application code should compare tokens but should not derive business meaning from the number. Timestamps are a poor substitute. Two application hosts can disagree about the current time, clocks can move and two acquisitions can receive the same timestamp at the available precision. A random GUID is unique but has no useful ordering. A database `rowversion` changes in order, but it describes changes to a database row rather than ownership generations for a named resource.

The safest issuer is usually the same strongly consistent component that grants the lease. A SQL transaction can update the lease and increment the token together. A consensus backed lock service may expose the token directly. If the lock and counter are held in separate systems, failures between the two operations can make it unclear whether ownership was granted and which token belongs to it.

## A SQL Server lease table

The following design uses SQL Server as both the lease coordinator and token issuer. It works well when the application already depends on SQL Server and the lock volume is moderate. It also avoids adding a separate distributed system solely for coordination.

```sql
CREATE TABLE dbo.ResourceLeases
(
    ResourceName  nvarchar(200)  NOT NULL,
    OwnerId       uniqueidentifier NULL,
    ExpiresAtUtc  datetimeoffset(7) NULL,
    Fence         bigint         NOT NULL
        CONSTRAINT DF_ResourceLeases_Fence DEFAULT (0),

    CONSTRAINT PK_ResourceLeases
        PRIMARY KEY (ResourceName)
);
```

`ResourceName` identifies the protected operation, such as `statement:account-1842:2026-07`. `OwnerId` identifies one acquisition attempt or worker execution. `Fence` survives release and expiry because the next owner must receive a greater value than every previous owner. The database should decide whether the lease has expired. Comparing `ExpiresAtUtc` with `SYSUTCDATETIME()` avoids making ownership depend on the clocks of individual application instances.

## Acquiring the lease and token together

The acquisition procedure creates the resource row if necessary, locks the relevant key range and increments the token only when the previous lease is available or expired.

```sql
CREATE OR ALTER PROCEDURE dbo.TryAcquireResourceLease
    @ResourceName nvarchar(200),
    @OwnerId uniqueidentifier,
    @LeaseSeconds int
AS
BEGIN
    SET NOCOUNT ON;
    SET XACT_ABORT ON;

    IF @LeaseSeconds < 5 OR @LeaseSeconds > 300
        THROW 51000, 'Lease duration must be between 5 and 300 seconds.', 1;

    BEGIN TRANSACTION;

    DECLARE @Now datetimeoffset(7) = SYSUTCDATETIME();

    IF NOT EXISTS
    (
        SELECT 1
        FROM dbo.ResourceLeases WITH (UPDLOCK, HOLDLOCK)
        WHERE ResourceName = @ResourceName
    )
    BEGIN
        INSERT dbo.ResourceLeases
            (ResourceName, OwnerId, ExpiresAtUtc, Fence)
        VALUES
            (@ResourceName, NULL, NULL, 0);
    END;

    UPDATE dbo.ResourceLeases
    SET OwnerId = @OwnerId,
        ExpiresAtUtc = DATEADD(SECOND, @LeaseSeconds, @Now),
        Fence = Fence + 1
    OUTPUT
        INSERTED.Fence,
        INSERTED.ExpiresAtUtc
    WHERE ResourceName = @ResourceName
      AND
      (
          OwnerId IS NULL
          OR ExpiresAtUtc <= @Now
      );

    COMMIT TRANSACTION;
END;
```

The procedure returns one row when acquisition succeeds and no rows when another owner still holds the lease. `UPDLOCK` and `HOLDLOCK` protect creation of a previously unseen resource row. The primary-key index is important because SQL Server needs an index range to lock when the row does not yet exist. The acquisition should normally use a fresh `OwnerId`. Reusing a process wide identifier makes it harder to distinguish a current acquisition from an abandoned one after the process has paused or retried.

## Calling it from .NET

The .NET contract returns the resource, acquisition owner, token and database-calculated expiry together.

```csharp
public sealed record FencedLease(
    string ResourceName,
    Guid OwnerId,
    long Token,
    DateTimeOffset ExpiresAtUtc);
```

The implementation below uses `Microsoft.Data.SqlClient`. A missing result row means that the resource is already leased.

```csharp
using System.Data;
using Microsoft.Data.SqlClient;

public sealed class SqlFencedLeaseManager(string connectionString)
{
    public async Task<FencedLease?> TryAcquireAsync(
        string resourceName,
        TimeSpan duration,
        CancellationToken stopToken)
    {
        var ownerId = Guid.NewGuid();
        await using var connection = new SqlConnection(connectionString);
        await connection.OpenAsync(stopToken);
        await using var command = new SqlCommand(
            "dbo.TryAcquireResourceLease",
            connection)
        {
            CommandType = CommandType.StoredProcedure
        };

        command.Parameters.Add(
            new SqlParameter("@ResourceName", SqlDbType.NVarChar, 200)
            {
                Value = resourceName
            });

        command.Parameters.Add(
            new SqlParameter("@OwnerId", SqlDbType.UniqueIdentifier)
            {
                Value = ownerId
            });

        command.Parameters.Add(
            new SqlParameter("@LeaseSeconds", SqlDbType.Int)
            {
                Value = checked((int)duration.TotalSeconds)
            });

        await using var reader = await command.ExecuteReaderAsync(stopToken);

        if (!await reader.ReadAsync(stopToken))
        {
            return null;
        }

        return new FencedLease(
            resourceName,
            ownerId,
            reader.GetInt64(0),
            reader.GetFieldValue<DateTimeOffset>(1));
    }
}
```

Obtaining the lease still does not protect anything on its own. The next step carries `lease.Token` to the component that owns the shared state.

## Enforcing the token at the write boundary

Assume the generated account statement is represented by a row whose payload must only be updated by the latest lock generation.

```sql
CREATE TABLE dbo.AccountStatements
(
    StatementId       uniqueidentifier NOT NULL,
    Content            varbinary(max)   NOT NULL,
    LastAcceptedFence  bigint           NOT NULL
        CONSTRAINT DF_AccountStatements_LastFence DEFAULT (0),

    CONSTRAINT PK_AccountStatements
        PRIMARY KEY (StatementId)
);
```

The protected update compares the incoming token and records it in the same SQL statement.

```sql
CREATE OR ALTER PROCEDURE dbo.SaveAccountStatement
    @StatementId uniqueidentifier,
    @Content varbinary(max),
    @Fence bigint
AS
BEGIN
    SET NOCOUNT ON;

    UPDATE dbo.AccountStatements
    SET Content = @Content,
        LastAcceptedFence = @Fence
    WHERE StatementId = @StatementId
      AND @Fence >= LastAcceptedFence;

    IF @@ROWCOUNT = 0
        THROW 51001, 'The write was rejected because its fencing token is stale.', 1;
END;
```

Once token `42` has updated the row, a delayed update carrying `41` affects zero rows and is rejected. The decision is made next to the data and is atomic with the change. The comparison uses `>=` because one owner may perform several legitimate writes while holding the same lease. Equal tokens remain valid; lower tokens do not. That choice leaves two additional concerns:

*   A retried write can execute more than once, so operations with non-idempotent effects still need an idempotency key.
    
*   Concurrent commands from the same owner are not ordered by the fence. If their order is significant, add an operation sequence or serialise them inside the owner.
    

For a design in which each acquisition is allowed exactly one final write, use `>` instead and treat an equal token as an already-applied attempt. The comparison must match the operation contract rather than being copied mechanically.

## Renewal does not issue a new token

Renewing a lease extends the current ownership generation. It should keep the same fencing token.

```sql
CREATE OR ALTER PROCEDURE dbo.TryRenewResourceLease
    @ResourceName nvarchar(200),
    @OwnerId uniqueidentifier,
    @Fence bigint,
    @LeaseSeconds int
AS
BEGIN
    SET NOCOUNT ON;

    DECLARE @Now datetimeoffset(7) = SYSUTCDATETIME();

    UPDATE dbo.ResourceLeases
    SET ExpiresAtUtc = DATEADD(SECOND, @LeaseSeconds, @Now)
    WHERE ResourceName = @ResourceName
      AND OwnerId = @OwnerId
      AND Fence = @Fence
      AND ExpiresAtUtc > @Now;

    SELECT CAST(IIF(@@ROWCOUNT = 1, 1, 0) AS bit) AS Renewed;
END;
```

Matching both `OwnerId` and `Fence` prevents an old execution from renewing a later owner's lease. Requiring the current lease to be unexpired avoids silently reviving an ownership generation after the application has already lost it. If renewal fails, the worker should stop as soon as it can. Cancellation is useful for reducing wasted work, but the fenced write remains necessary because the worker may already be paused or may ignore cancellation while executing non-cooperative code.

Release follows the same ownership check:

```sql
UPDATE dbo.ResourceLeases
SET OwnerId = NULL,
    ExpiresAtUtc = NULL
WHERE ResourceName = @ResourceName
  AND OwnerId = @OwnerId
  AND Fence = @Fence;
```

An old worker must never be able to release the current worker's lease.

## Fencing and optimistic concurrency solve different problems

EF Core concurrency tokens, including SQL Server `rowversion`, detect that a row has changed since it was read. That protects an edit based on an outdated snapshot. Fencing tokens establish whether a writer belongs to an obsolete ownership generation. The two controls can appear in the same update. A fence can confirm that the worker is still the newest lock owner, while a `rowversion` confirms that the particular data on which its calculation was based has not changed. For example, a worker with the current fence may still have calculated a statement from an outdated account balance. Passing the fencing check alone does not make its business inputs current. Conversely, passing a `rowversion` check does not prove that the worker still owns an external lease.

## The shared resource has to understand the token

Fencing works only when the component applying the side effect can reject a stale token. A SQL row can do this in its `UPDATE`. An internal HTTP service can require an `X-Fencing-Token` header and store the highest token beside the resource. A storage system may offer conditional writes based on a version or lease identifier. Some destinations provide no such mechanism. A third-party API that accepts an unconditional `POST`, for example, cannot be made safe merely by checking the token in the calling service. Ownership can change after that check but before the request arrives.

When the destination cannot validate a fencing token directly, the protection has to move elsewhere in the architecture. One approach is to route all writes through a gateway that owns access to the destination and rejects operations from older ownership generations. Where the destination supports idempotency keys or conditional writes, those capabilities can also be used, provided their semantics are strong enough to prevent stale operations from replacing newer results.

Another option is to record the operation in a fenced local store and allow a controlled dispatcher to perform the external side effect. Where possible, the operation can also be redesigned so delayed results are harmless. For example, each worker can write an immutable, versioned object, while a separately fenced update changes the pointer identifying the current version.

The last option works particularly well for files. Each worker writes to a unique immutable blob, while a small database row identifies which blob is current. Only the pointer update needs fencing. An older worker may leave an unused blob, but it cannot replace the current version. A cleanup process can remove unreferenced blobs later.

Azure Blob Storage leases deserve a distinction here. A blob lease gives exclusive write and delete access to that blob, and writes against an actively leased blob must include the matching lease ID. Blob Storage therefore performs the ownership check for operations on that blob. The lease ID is not a general monotonically increasing fencing token that can be passed to unrelated resources. If a blob lease coordinates a database update or an external API call, those destinations still require their own protection.

## Redis locks still need the protected resource check

A Redis lock created with a unique owner value and expiry can be a useful coordination mechanism. It can tell workers which one should proceed, and a compare-and-delete operation can prevent one owner from releasing another owner's lock. It doesnt prevent an expired owner from writing to a separate database after it resumes. Redis's own distributed lock documentation recommends fencing tokens for this reason and warns against assuming that a lock remains held for as long as the owning process is alive.

Adding `INCR` after acquiring a Redis lock is not automatically enough. The application must define what happens if lock acquisition succeeds and the process fails before incrementing the counter, or if the counter succeeds and the acquisition result is lost. The consistency and failover behaviour of the token issuer also need to support the ordering guarantee the protected resource relies on. A single atomic operation or a lock implementation that returns a fencing token is easier to reason about.

## Failure handling in the application

A stale token rejection is expected concurrency behaviour. It should not be retried with the same token in the hope that it eventually succeeds. The work belongs to an obsolete owner. The handler should record enough context to explain the rejection: resource name, owner ID, fencing token, operation ID and the stage at which the write failed. The payload itself may need secure retention or redaction, but the ownership metadata is valuable when reconstructing the event. The current owner can retry transient infrastructure failures with its existing token while its lease remains valid. If the lease has expired or renewal status is uncertain, the worker should abandon the attempt and reacquire. Reacquisition creates a new owner ID and a new fencing token; it is a new execution generation. Avoid silently reacquiring halfway through a critical section. Earlier reads and calculations belong to the previous generation and may no longer be valid. Restarting the operation from a defined boundary is usually safer.

## Testing the failure rather than only the happy path

Unit tests can verify token comparisons, but the most useful tests involve at least two application instances and the real coordination store.

One integration test should perform this sequence:

1.  Worker A acquires token `41`.
    
2.  The test prevents A from renewing and waits for expiry.
    
3.  Worker B acquires token `42` and writes successfully.
    
4.  Worker A resumes and attempts its write with token `41`.
    
5.  The resource rejects A and retains B's data.
    

Additional tests should prove that renewal preserves the token, an old owner cannot renew or release a later lease, equal-token behaviour matches the chosen operation contract and concurrent first-time acquisitions create only one resource row. The pause should happen after useful work has started, not before lock acquisition. Otherwise, the test demonstrates ordinary lock contention rather than the stale-owner failure fencing is intended to stop.

## When the extra machinery is justified

Fencing is most useful when work can outlive a lease, pauses are plausible and a late write would corrupt or replace newer state. File generation, scheduled billing, settlement, document processing, infrastructure reconciliation and long running imports are common examples. It may be unnecessary when the protected operation and lock are contained in one short database transaction. The database already owns the isolation boundary in that case. It can also be redundant when the destination itself requires a currently valid lease ID on every protected operation, as Azure Blob Storage does for writes to a leased blob. The important question is whether an old worker can still reach the side effect after a new worker has taken ownership. If it can, preventing simultaneous acquisition is only half of the design.

## Closing the stale owner gap

A distributed lease helps the system choose a worker. A fencing token lets the resource refuse workers whose choice is no longer current. The implementation is small, increment an ownership generation during acquisition, carry it with every protected operation, and compare it atomically where the change is applied. The surrounding decisions require more care. Renewal must retain the same token, release must verify the owner, repeated operations need defined equality semantics, and external destinations must offer somewhere to enforce the comparison. Once those conditions are in place, a paused process can resume and attempt to continue. It simply cannot overwrite the work of the owner that replaced it.

*   [Redis: Distributed Locks](https://redis.io/docs/latest/develop/clients/patterns/distributed-locks/)
    
*   [Hazelcast: FencedLock](https://docs.hazelcast.com/hazelcast/5.5/data-structures/fencedlock)
    
*   [Microsoft Learn: Create and Manage Blob Leases with .NET](https://learn.microsoft.com/en-us/azure/storage/blobs/storage-blob-lease)
    
*   [Microsoft Learn: Handling Concurrency Conflicts in EF Core](https://learn.microsoft.com/en-us/ef/core/saving/concurrency)
