Skip to content

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:

typescript
// ❌ 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:

typescript
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

typescript
// Ensure job runs exactly once across workers
await lock(() => processJob(jobId), { key: `job:${jobId}` });

Idempotency enforcement

typescript
// Webhook received multiple times? Handle once
await lock(() => handleWebhook(eventId), { key: `webhook:${eventId}` });

Rate limiting

typescript
// 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

typescript
// Daily report generated by any instance, but only once
await lock(() => generateDailyReport(), {
  key: "cron:daily-report",
  ttlMs: 300000,
});

Resource coordination

typescript
// 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 ​

  1. Acquire: Request lock with unique key (e.g., payment:123)
  2. Execute: Run your critical section while holding the lock
  3. 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.