Getting Started
Get your first distributed lock running in under 5 minutes.
Installation
npm install syncguard ioredisnpm install syncguard postgresnpm install syncguard @google-cloud/firestoreInstall only the backend you need. Peer dependencies are optional.
Quick Start (Redis)
import { createLock } from "syncguard/redis";
import Redis from "ioredis";
const redis = new Redis();
const lock = createLock(redis);
// Prevent duplicate payment processing
await lock(
async () => {
const payment = await getPayment(paymentId);
if (payment.status === "pending") {
await chargeCard(payment.amount);
await updateStatus(paymentId, "completed");
}
},
{ key: `payment:${paymentId}` },
);Quick Start (PostgreSQL)
import { createLock, setupSchema } from "syncguard/postgres";
import postgres from "postgres";
const sql = postgres("postgresql://localhost:5432/myapp");
// Setup schema (once, during initialization)
await setupSchema(sql);
// Create lock function (synchronous)
const lock = createLock(sql);
await lock(
async () => {
// Your critical section
},
{ key: "resource:123" },
);PostgreSQL Schema Setup
Call setupSchema() once during application initialization to create required tables and indexes. This is an idempotent operation safe to call multiple times. See postgres/schema.sql for complete table and index definitions.
Quick Start (Firestore)
import { createLock } from "syncguard/firestore";
import { Firestore } from "@google-cloud/firestore";
const db = new Firestore();
const lock = createLock(db);
await lock(
async () => {
// Your critical section
},
{ key: "resource:123" },
);Firestore Index Required
For optimal performance, create a single-field ascending index on the lockId field in your locks collection. Firestore typically auto-creates this for equality queries, but verify in production.
That's it. The lock() function handles acquisition, retries, execution, and release automatically.
Configuration Basics
Customize lock behavior with inline options:
await lock(
async () => {
// Your work function
},
{
key: "job:daily-report", // Required: unique lock identifier
ttlMs: 60000, // Lock expires after 60s (default: 30s)
acquisition: {
timeoutMs: 10000, // Give up acquisition after 10s (default: 5s)
maxRetries: 20, // Retry up to 20 times on contention (default: 10)
retryDelayMs: 100, // Initial retry delay (default: 100ms)
backoff: "exponential", // Backoff strategy (default: "exponential")
jitter: "equal", // Jitter strategy (default: "equal")
},
},
);Key guidelines:
ttlMs: Short enough to minimize impact of crashed processes, long enough for your workacquisition.timeoutMs: How long to wait for contended locks before giving upacquisition.maxRetries: Higher = more patient under load; uses exponential backoff with jitter
Backend-specific config (collection names, key prefixes):
const lock = createLock(redis, {
keyPrefix: "my-app", // Default: "syncguard"
});// Setup schema with custom table names
await setupSchema(sql, {
tableName: "app_locks",
fenceTableName: "app_fence_counters",
});
// Create lock with matching config
const lock = createLock(sql, {
tableName: "app_locks", // Default: "syncguard_locks"
fenceTableName: "app_fence_counters", // Default: "syncguard_fence_counters"
});const lock = createLock(db, {
collection: "app_locks", // Default: "locks"
fenceCollection: "app_fences", // Default: "fence_counters"
});Manual Lock Control
For long-running tasks or custom retry logic, use the backend directly with automatic cleanup. Note that the lock() function from the Quick Start is a high-level wrapper, while createRedisBackend (and similar createXBackend functions) provide direct access to the backend's lower-level operations:
import { createRedisBackend } from "syncguard/redis";
const backend = createRedisBackend(redis);
// Acquire lock with automatic cleanup (Node.js ≥20)
{
await using lock = await backend.acquire({
key: "batch:daily-report",
ttlMs: 300000, // 5 minutes
});
if (lock.ok) {
// TypeScript narrows lock to include handle methods after ok check
const { fence } = lock;
await generateReport(fence);
// Extend lock for long-running tasks
await lock.extend(300000); // Another 5 minutes
await sendReportEmail();
// Lock automatically released here
} else {
console.log("Resource locked by another process");
}
}For older runtimes (Node.js <20), use try/finally
const result = await backend.acquire({
key: "batch:daily-report",
ttlMs: 300000,
});
if (result.ok) {
try {
const { lockId, fence } = result;
await generateReport(fence);
const extended = await backend.extend({
lockId,
ttlMs: 300000,
});
if (!extended.ok) {
throw new Error("Failed to extend lock");
}
await sendReportEmail();
} finally {
await backend.release({ lockId: result.lockId });
}
} else {
console.log("Resource locked by another process");
}Error handling for disposal:
SyncGuard provides a safe-by-default error handler that automatically logs disposal failures in development mode (NODE_ENV !== 'production'). In production, enable logging with SYNCGUARD_DEBUG=true or provide a custom onReleaseError callback:
const backend = createRedisBackend(redis, {
onReleaseError: (error, context) => {
logger.error("Failed to release lock", {
error: error.message,
lockId: context.lockId,
key: context.key,
});
},
});Why manual mode?
- Extending locks during long-running work
- Custom retry strategies
- Conditional lock release
- Access to fencing tokens (see Fencing Tokens)
Error Handling
Lock operations throw LockError for system failures:
import { LockError } from "syncguard";
try {
await lock(
async () => {
// Critical section
},
{ key: "resource:123" },
);
} catch (error) {
if (error instanceof LockError) {
console.error(`[${error.code}] ${error.message}`);
// Handle specific error codes
if (error.code === "AcquisitionTimeout") {
// Contention exceeded timeout
} else if (error.code === "ServiceUnavailable") {
// Backend unavailable, retry later
}
}
}Common error codes:
AcquisitionTimeout: Couldn't acquire lock withintimeoutMs(contention)ServiceUnavailable: Backend unavailable (network/connection issues)NetworkTimeout: Operation timed out (client-side timeout)InvalidArgument: Invalid parameters (malformed key/lockId)
TIP
See API Reference for all error codes. For backend error mapping specifications, see docs/specs/interface.md § Error Handling.