# @upstash/lock `@upstash/lock` is a distributed lock and debounce library built on top of Upstash Redis. It provides mutex-style locking mechanisms for coordinating access to shared resources across multiple instances of an application, ensuring that critical sections of code are executed by only one process at a time. The library is designed for serverless and edge computing environments where traditional in-memory locks are insufficient. The library offers two primary utilities: a `Lock` class for distributed mutual exclusion and a `Debounce` class for rate-limiting function executions across distributed systems. Both utilities leverage Redis atomic operations and Lua scripts to guarantee correctness in concurrent environments. Note that this implementation is suitable for efficiency purposes (avoiding duplicate work) rather than strict correctness guarantees like leader election, due to Redis's async replication model. ## Installation ```bash npm install @upstash/lock ``` ## Lock Constructor Creates a new distributed lock instance with configurable lease duration and retry behavior. The lock uses Redis SET with NX (not exists) and PX (expiration in milliseconds) options to ensure atomic lock acquisition. ```typescript import { Lock } from "@upstash/lock"; import { Redis } from "@upstash/redis"; // Create a lock with default settings (10s lease, 3 retry attempts, 100ms delay) const lock = new Lock({ id: "my-resource-lock", redis: Redis.fromEnv(), }); // Create a lock with custom configuration const customLock = new Lock({ id: "custom-resource-lock", redis: new Redis({ url: "https://your-redis-url.upstash.io", token: "your-token", }), lease: 5000, // Lock expires after 5 seconds retry: { attempts: 5, // Try up to 5 times to acquire delay: 200, // Wait 200ms between attempts }, }); ``` ## Lock.acquire() Attempts to acquire the lock with optional configuration overrides. Returns `true` if the lock was successfully acquired, `false` otherwise. The method will automatically retry based on the configured retry settings. ```typescript import { Lock } from "@upstash/lock"; import { Redis } from "@upstash/redis"; const lock = new Lock({ id: "payment-processing-lock", redis: Redis.fromEnv(), }); async function processPayment(orderId: string) { // Acquire with default settings const acquired = await lock.acquire(); if (acquired) { try { // Critical section - only one instance executes this at a time await processPaymentForOrder(orderId); } finally { await lock.release(); } } else { console.log("Could not acquire lock, another instance is processing"); } } // Override settings at acquisition time async function processUrgentPayment(orderId: string) { const acquired = await lock.acquire({ lease: 30000, // Longer lease for complex operations retry: { attempts: 10, // More retries for urgent tasks delay: 50, // Shorter delay between retries }, }); if (acquired) { try { await processPaymentForOrder(orderId); } finally { await lock.release(); } } } ``` ## Lock.release() Safely releases the lock by verifying the UUID matches before deletion. Uses a Lua script for atomic check-and-delete operation, ensuring only the lock owner can release it. ```typescript import { Lock } from "@upstash/lock"; import { Redis } from "@upstash/redis"; const lock = new Lock({ id: "inventory-update-lock", redis: Redis.fromEnv(), }); async function updateInventory(productId: string, quantity: number) { if (await lock.acquire()) { try { // Perform inventory update await decrementStock(productId, quantity); console.log("Inventory updated successfully"); } catch (error) { console.error("Error updating inventory:", error); } finally { // Always release the lock const released = await lock.release(); if (released) { console.log("Lock released successfully"); } else { console.log("Lock was already released or expired"); } } } } ``` ## Lock.extend() Extends the lock's lease duration by the specified number of milliseconds. Useful for long-running operations that may exceed the initial lease time. Returns `true` if the extension was successful. ```typescript import { Lock } from "@upstash/lock"; import { Redis } from "@upstash/redis"; const lock = new Lock({ id: "batch-job-lock", redis: Redis.fromEnv(), lease: 10000, // Initial 10 second lease }); async function processBatchJob(items: string[]) { if (await lock.acquire()) { try { for (let i = 0; i < items.length; i++) { await processItem(items[i]); // Extend lock every 5 items to prevent expiration during long jobs if (i % 5 === 0) { const extended = await lock.extend(10000); // Add 10 more seconds if (!extended) { console.log("Failed to extend lock, aborting batch"); break; } } } } finally { await lock.release(); } } } ``` ## Lock.getStatus() Returns the current status of the lock: `"ACQUIRED"` if this instance holds the lock, or `"FREE"` if the lock is available or held by another instance. ```typescript import { Lock } from "@upstash/lock"; import { Redis } from "@upstash/redis"; const lock = new Lock({ id: "resource-lock", redis: Redis.fromEnv(), }); async function checkAndAcquire() { // Check initial status const initialStatus = await lock.getStatus(); console.log(`Initial lock status: ${initialStatus}`); // Output: "FREE" // Acquire the lock await lock.acquire(); // Check status after acquisition const acquiredStatus = await lock.getStatus(); console.log(`After acquire: ${acquiredStatus}`); // Output: "ACQUIRED" // Release the lock await lock.release(); // Check status after release const releasedStatus = await lock.getStatus(); console.log(`After release: ${releasedStatus}`); // Output: "FREE" } ``` ## Debounce Constructor Creates a distributed debounce instance that ensures a callback function is executed at most once within a specified wait period, even across multiple application instances. ```typescript import { Debounce } from "@upstash/lock"; import { Redis } from "@upstash/redis"; // Create a debounced function with 1 second wait (default) const debouncedSync = new Debounce({ id: "sync-operation", redis: Redis.fromEnv(), wait: 1000, callback: async (userId: string) => { console.log(`Syncing data for user: ${userId}`); await syncUserData(userId); }, }); // Create a debounced notification sender const debouncedNotification = new Debounce({ id: "notification-sender", redis: new Redis({ url: "https://your-redis-url.upstash.io", token: "your-token", }), wait: 5000, // Wait 5 seconds before sending callback: async (message: string) => { await sendNotification(message); }, }); ``` ## Debounce.call() Triggers the debounced function. If called multiple times within the wait period, only the last invocation's callback will be executed. Note that there is always a delay of `wait` milliseconds before the callback executes. ```typescript import { Debounce } from "@upstash/lock"; import { Redis } from "@upstash/redis"; // Debounce expensive search index updates const debouncedIndexUpdate = new Debounce({ id: "search-index-update", redis: Redis.fromEnv(), wait: 2000, // Wait 2 seconds callback: async (documentId: string) => { console.log(`Updating search index for document: ${documentId}`); await updateSearchIndex(documentId); }, }); // In your API handler - multiple rapid calls result in single execution async function handleDocumentUpdate(documentId: string) { // This will only trigger the callback once per 2-second window // Even if called from multiple server instances await debouncedIndexUpdate.call(documentId); } // Example: Multiple calls within wait period async function batchUpdates() { const debounced = new Debounce({ id: "batch-counter", redis: Redis.fromEnv(), wait: 1000, callback: () => { console.log("Callback executed only once!"); }, }); // All these calls happen within 1 second for (let i = 0; i < 10; i++) { debounced.call(); } // Wait for debounce to complete await new Promise(resolve => setTimeout(resolve, 2000)); // Output: "Callback executed only once!" } ``` ## Complete Lock Workflow Example Demonstrates a full lock lifecycle with error handling and status checking for a critical database operation. ```typescript import { Lock } from "@upstash/lock"; import { Redis } from "@upstash/redis"; const redis = Redis.fromEnv(); async function performCriticalDatabaseMigration(migrationId: string) { const lock = new Lock({ id: `migration-${migrationId}`, redis, lease: 60000, // 60 second lease for long migration retry: { attempts: 3, delay: 1000, // Wait 1 second between retries }, }); console.log(`Lock status before acquire: ${await lock.getStatus()}`); const acquired = await lock.acquire(); if (!acquired) { console.log("Migration already in progress by another instance"); return { success: false, reason: "lock_unavailable" }; } console.log(`Lock status after acquire: ${await lock.getStatus()}`); try { // Step 1: Backup console.log("Creating backup..."); await createBackup(); await lock.extend(30000); // Extend lease for next phase // Step 2: Migrate console.log("Running migration..."); await runMigration(migrationId); await lock.extend(30000); // Extend lease for verification // Step 3: Verify console.log("Verifying migration..."); await verifyMigration(); return { success: true }; } catch (error) { console.error("Migration failed:", error); await rollbackMigration(); return { success: false, reason: "migration_error", error }; } finally { const released = await lock.release(); console.log(`Lock released: ${released}`); console.log(`Lock status after release: ${await lock.getStatus()}`); } } // Usage performCriticalDatabaseMigration("v2.0.0").then(result => { console.log("Migration result:", result); }); ``` ## Summary The `@upstash/lock` library is ideal for serverless applications, edge functions, and distributed systems where multiple instances need to coordinate access to shared resources. Common use cases include preventing duplicate job execution in queue workers, rate-limiting expensive API calls across instances, coordinating database migrations, implementing distributed caching refresh patterns, and ensuring single-execution of scheduled tasks. The debounce utility is particularly useful for aggregating rapid-fire events like search index updates or notification batching. Integration is straightforward with any Upstash Redis instance using either environment variables (`Redis.fromEnv()`) or explicit configuration. The library works seamlessly with serverless platforms like Vercel, AWS Lambda, and Cloudflare Workers. For production use, configure appropriate lease durations based on your operation's expected runtime, and implement proper error handling to ensure locks are always released even when exceptions occur. Remember that this implementation provides best-effort guarantees suitable for efficiency optimization rather than strict correctness requirements.