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
- A client calls
acquireLock()with a mutex string identifying the shared resource - Squid atomically acquires the lock in Redis
- If another client already holds the lock, the request waits until the lock is released or the timeout expires
- The lock is automatically renewed in the background while held
- The lock is released explicitly by calling
release(), or automatically if the client disconnects
When to use distributed locks
| Use Case | Recommendation |
|---|---|
| Prevent concurrent modifications to a shared resource | Distributed lock |
| Increment counters or update balances safely | Distributed lock |
| Coordinate exclusive access across multiple clients | Distributed lock |
| Atomic single-document updates | Use Database transactions |
| Enforce rate limits on backend functions | Use Rate and quota limiting |
Quick Start
Prerequisites
- A Squid frontend project with
@squidcloud/clientinstalled - 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:
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
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.
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:
const lock = await squid.acquireLock('my-resource', {
acquisitionTimeoutMillis: 5000, // Wait up to 5 seconds to acquire
maxHoldTimeMillis: 30000, // Auto-release after 30 seconds
});
| Option | Type | Default | Description |
|---|---|---|---|
acquisitionTimeoutMillis | number | 2000 | Maximum time in milliseconds to wait for the lock to become available. If the lock is not acquired within this time, the promise rejects. |
maxHoldTimeMillis | number | No limit | Maximum 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 / Method | Type | Description |
|---|---|---|
resourceId | string | The mutex string identifying this lock |
lockId | string | A unique identifier for this lock instance |
release() | Promise<void> | Releases the lock |
isReleased() | boolean | Returns 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:
lock.release();
The lock is also released automatically when:
- The client disconnects from the server
- The
maxHoldTimeMillisduration expires - The Squid client instance is destroyed
Checking lock status
Use isReleased() to check whether a lock has been released:
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:
// 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:
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:
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:
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
| Error | Cause | Solution |
|---|---|---|
LOCK_TIMEOUT | Another client holds the lock longer than acquisitionTimeoutMillis | Increase the timeout, or implement retry logic |
| Client not connected | WebSocket connection is not established | Ensure the Squid client is initialized and connected before acquiring locks |
| Unauthorized | No security rule allows lock access for this mutex | Add 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.
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:
// 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:
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:
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.