Skip to main content

Distributed locks

Manage access to your shared resources in real-time to transact data in the desired order

Why Use Distributed Locks

Two users click "Buy" on the last item in stock at the same time. Both read the stock as 1, both decrement it, and both orders go through, even though only one item exists. This is a race condition: two operations that must run sequentially are running concurrently.

// Without a lock: race condition
const stock = await getStock(itemId); // Both clients read 1
await setStock(itemId, stock - 1); // Both clients write 0 -- two orders, one item
// With a lock: sequential access
const lock = await squid.acquireLock('inventory-item-123');
try {
const stock = await getStock(itemId); // Only one client reads at a time
if (stock > 0) {
await setStock(itemId, stock - 1);
}
} finally {
lock.release();
}

Distributed locks ensure only one client holds access to a shared resource at a time, preventing data corruption from concurrent operations.

Overview

Squid provides distributed locks that coordinate access across multiple clients and application instances. When a client acquires a lock, all other clients requesting the same lock wait until it is released.

How it works

  1. A client calls acquireLock() with a mutex string identifying the shared resource
  2. Squid atomically acquires the lock in Redis
  3. If another client already holds the lock, the request waits until the lock is released or the timeout expires
  4. The lock is automatically renewed in the background while held
  5. The lock is released explicitly by calling release(), or automatically if the client disconnects

When to use distributed locks

Use CaseRecommendation
Prevent concurrent modifications to a shared resourceDistributed lock
Increment counters or update balances safelyDistributed lock
Coordinate exclusive access across multiple clientsDistributed lock
Atomic single-document updatesUse Database transactions
Enforce rate limits on backend functionsUse Rate and quota limiting

Quick Start

Prerequisites

  • A Squid frontend project with @squidcloud/client installed
  • A Squid backend project with a security rule allowing lock access

Step 1: Authorize lock access in the backend

By default, all distributed locks are denied. Add a security rule to allow access:

Backend code
import { secureDistributedLock, SquidService } from '@squidcloud/backend';

export class ExampleService extends SquidService {
@secureDistributedLock()
allowLocks(): boolean {
return this.isAuthenticated();
}
}

Step 2: Deploy the backend

For local development, run the backend locally using the Squid CLI:

squid start

To deploy to the cloud, see deploying your backend.

Step 3: Acquire and use the lock on the client

Client code
const lock = await squid.acquireLock('my-resource');
try {
// Safely read and update a shared resource
} finally {
lock.release();
}

Core Concepts

Acquiring a lock

Use acquireLock() to acquire a distributed lock for a given mutex. The mutex is a string that uniquely identifies the shared resource you want to protect.

Client code
const lock = await squid.acquireLock('my-resource');

If another client already holds the lock, the promise waits until the lock becomes available or the acquisition timeout expires.

Lock options

acquireLock() accepts an optional second parameter to configure lock behavior:

Client code
const lock = await squid.acquireLock('my-resource', {
acquisitionTimeoutMillis: 5000, // Wait up to 5 seconds to acquire
maxHoldTimeMillis: 30000, // Auto-release after 30 seconds
});
OptionTypeDefaultDescription
acquisitionTimeoutMillisnumber2000Maximum time in milliseconds to wait for the lock to become available. If the lock is not acquired within this time, the promise rejects.
maxHoldTimeMillisnumberNo limitMaximum time in milliseconds the lock can be held before it is automatically released. Useful as a safety net to prevent locks from being held indefinitely.

The DistributedLock object

When acquireLock() resolves, it returns a DistributedLock object with the following interface:

Property / MethodTypeDescription
resourceIdstringThe mutex string identifying this lock
lockIdstringA unique identifier for this lock instance
release()Promise<void>Releases the lock
isReleased()booleanReturns true if the lock has been released
observeRelease()Observable<void>Emits when the lock is released

Releasing a lock

Call release() to free the lock so other clients can acquire it:

Client code
lock.release();

The lock is also released automatically when:

  • The client disconnects from the server
  • The maxHoldTimeMillis duration expires
  • The Squid client instance is destroyed

Checking lock status

Use isReleased() to check whether a lock has been released:

Client code
console.log(lock.isReleased()); // true or false

Observing lock release

Use observeRelease() to react when a lock is released, whether explicitly or due to disconnection:

Client code
// observeRelease() returns an RxJS Observable that emits once when released
lock.observeRelease().subscribe(() => {
console.log('Lock released, resource is now available');
});

Managing a lock with a callback

Use withLock() to acquire a lock, execute a callback, and automatically release the lock when the callback completes. This eliminates the need for manual try/finally blocks:

Client code
const result = await squid.withLock('my-resource', async (lock) => {
// The lock is held for the duration of this callback
const data = await readSharedData();
await updateSharedData(data + 1);
return data + 1;
});
// Lock is automatically released here, even if the callback throws

The callback receives the DistributedLock object as a parameter, allowing you to check its status or observe release events within the callback.

Error Handling

Acquisition timeout

If the lock cannot be acquired within acquisitionTimeoutMillis (default: 2 seconds), the promise rejects with a LOCK_TIMEOUT error:

Client code
try {
const lock = await squid.acquireLock('busy-resource', {
acquisitionTimeoutMillis: 3000,
});
// Use the lock...
lock.release();
} catch (error) {
console.error('Could not acquire lock:', error.message);
// Handle timeout: retry later, notify the user, etc.
}

Connection loss

If the client loses its connection to the server, all held locks are released automatically. The observeRelease() observable emits in this case, allowing you to detect unexpected releases:

Client code
const lock = await squid.acquireLock('my-resource');

let releasedByUs = false;
lock.observeRelease().subscribe(() => {
if (!releasedByUs) {
console.warn('Lock was released unexpectedly (possible disconnection)');
}
});

// Later, when releasing intentionally:
releasedByUs = true;
lock.release();

Common errors

ErrorCauseSolution
LOCK_TIMEOUTAnother client holds the lock longer than acquisitionTimeoutMillisIncrease the timeout, or implement retry logic
Client not connectedWebSocket connection is not establishedEnsure the Squid client is initialized and connected before acquiring locks
UnauthorizedNo security rule allows lock access for this mutexAdd a @secureDistributedLock rule in the backend

Best Practices

Always release in a finally block

Wrap lock usage in try/finally to ensure the lock is released even if an error occurs. Alternatively, use withLock() which handles this automatically.

Client code
const lock = await squid.acquireLock('my-resource');
try {
await performCriticalOperation();
} finally {
lock.release();
}

Use descriptive mutex names

Choose mutex names that clearly identify the protected resource. This makes debugging easier and prevents accidental collisions:

Client code
// Good: specific and descriptive
await squid.acquireLock('inventory-update-sku-12345');
await squid.acquireLock('user-balance-user-abc');

// Avoid: generic names that may collide
await squid.acquireLock('lock1');
await squid.acquireLock('update');

Set maxHoldTimeMillis as a safety net

Use maxHoldTimeMillis to prevent locks from being held indefinitely in case of bugs or unhandled errors:

Client code
const lock = await squid.acquireLock('critical-resource', {
maxHoldTimeMillis: 10000, // Force release after 10 seconds
});

Keep lock duration short

Hold locks for the minimum time necessary. Perform any non-critical work (logging, UI updates, notifications) outside the locked section:

Client code
const lock = await squid.acquireLock('shared-counter');
let newValue: number;
try {
const current = await readCounter();
newValue = current + 1;
await writeCounter(newValue);
} finally {
lock.release();
}
// Do non-critical work after releasing
console.log('Counter updated to', newValue);

Securing Distributed Locks

By default, distributed locks are fully secured and can only be acquired if a backend security rule allows it. Use the @secureDistributedLock decorator to control which clients can acquire which locks.

For detailed information on configuring lock security rules, including per-mutex authorization, see Securing distributed locks.