What is SyncGuard? β
SyncGuard is a TypeScript distributed lock library that prevents race conditions across microservices. It provides a simple API for coordinating access to shared resources using Redis or Firestore as the backend.
The Problem: Race Conditions in Distributed Systems β
When multiple services or processes work on the same resource, race conditions happen:
// β Without locks: payment processed twice
async function processPayment(paymentId: string) {
const payment = await getPayment(paymentId);
if (payment.status === "pending") {
await chargeCard(payment.amount); // Process 1 charges
await updateStatus(paymentId, "completed"); // Process 2 also charges!
}
}
Two instances read status: 'pending'
simultaneously. Both charge the card. Oops.
Why Database Transactions Aren't Enough
Database transactions don't help when the work involves external APIs (payment processors, emails, webhooks) or spans multiple databases.
The Solution: Distributed Locks β
SyncGuard ensures only one process executes the critical section:
import { createLock } from "syncguard/firestore";
const lock = createLock(db);
// β
With locks: only one process wins
await lock(
async () => {
const payment = await getPayment(paymentId);
if (payment.status === "pending") {
await chargeCard(payment.amount);
await updateStatus(paymentId, "completed");
}
},
{ key: `payment:${paymentId}`, ttlMs: 60000 },
);
The first process acquires the lock. Others wait or retry. Your customer gets charged once.
Key Features β
- π’ Fencing tokens β Monotonic counters prevent stale lock holders from corrupting data (see Fencing Tokens)
- π§Ή Automatic cleanup β TTL-based expiration means locks release even if your process crashes
- π Ownership tracking β Each lock gets a unique ID; only the owner can release or extend it
- π Backend flexibility β Use Redis for speed or Firestore for serverlessβsame API, same guarantees
- π Smart retries β Exponential backoff with jitter handles contention automatically
- π TypeScript-first β Compile-time type safety with capability inference
When to Use SyncGuard β
β Perfect For β
Preventing duplicate processing
// Ensure job runs exactly once across workers
await lock(() => processJob(jobId), { key: `job:${jobId}` });
Idempotency enforcement
// Webhook received multiple times? Handle once
await lock(() => handleWebhook(eventId), { key: `webhook:${eventId}` });
Rate limiting
// One request per user per minute
const result = await backend.acquire({
key: `rate:${userId}`,
ttlMs: 60000,
});
if (!result.ok) throw new Error("Rate limit exceeded");
Scheduled tasks
// Daily report generated by any instance, but only once
await lock(() => generateDailyReport(), {
key: "cron:daily-report",
ttlMs: 300000,
});
Resource coordination
// Prevent concurrent deploys
await lock(() => deployToProduction(), { key: "deploy:prod" });
When NOT to Use SyncGuard β
β Anti-Patterns β
High-Frequency Operations
Don't use locks for every API request or database query. Network round-trip per lock acquisition adds latency (~1-50ms). Consider optimistic concurrency or database locks instead.
Short critical sections
- If your critical section is faster than acquiring the lock, you're doing it wrong
- Example: incrementing a Redis counter (use
INCR
instead)
Within database transactions
- Database transactions already provide isolation
- Locks are for coordinating work outside a single database
As a queue replacement
- Locks coordinate access, they don't distribute work
- Use Redis Streams, SQS, Pub/Sub, etc. for job queues
Long-running workflows without extension
- Locks expire via TTL (default: 30s)
- For long tasks, use
backend.extend()
periodically or break into smaller steps
How It Works β
- Acquire: Request lock with unique key (e.g.,
payment:123
) - Execute: Run your critical section while holding the lock
- Release: Free the lock for others (or let TTL expire automatically)
Atomic Operations Guarantee
SyncGuard uses atomic operations (Lua scripts for Redis, transactions for Firestore) to ensure:
- No race window between checking and acquiring
- Ownership verified before release/extend (see ADR-003)
- Monotonic fence tokens for stale lock protection
Deep dive: See specs/interface.md for TOCTOU protection requirements and atomicity guarantees.