Orchestrating Distributed Tasks with PostgreSQL Advisory Locks
Takashi Yamamoto
Infrastructure Engineer · Leapcell

Introduction
In the world of distributed systems, coordinating tasks across multiple independent instances is a common yet challenging problem. Ensuring that only one instance performs a critical operation at a time, or managing shared resources without conflicts, requires a robust synchronization mechanism. Traditional approaches often involve dedicated queuing services or complex consensus algorithms, which can introduce additional infrastructure overhead and complexity. However, for many scenarios, a simpler, database-centric solution can be remarkably effective. This article delves into how PostgreSQL's advisory locks offer a powerful yet often overlooked primitive for achieving distributed task coordination, providing a lightweight, efficient, and readily available solution for applications already leveraging PostgreSQL.
Understanding Advisory Locks and Their Application
Before diving into the implementation, let's establish a clear understanding of the core concepts involved:
Advisory Locks are application-level locks managed by PostgreSQL. Unlike table or row locks, which are automatically acquired and released by the database transaction manager, advisory locks are explicit. They are acquired and released by the application code, giving developers fine-grained control over their lifecycle. Importantly, advisory locks are not tied to specific database objects and do not block regular SQL operations on tables or rows. They exist as arbitrary integer keys known to the application, making them perfect for defining application-specific synchronization points. PostgreSQL provides both session-level and transaction-level advisory locks. Session-level locks persist for the duration of the database session or until explicitly released, while transaction-level locks are automatically released at the end of the transaction. For distributed task coordination, session-level locks are typically more useful, as they allow a single instance to hold a lock across multiple operations or even for extended periods.
Distributed Task Coordination refers to the challenge of managing and synchronizing operations performed by multiple independent processes or services in a distributed environment. This includes scenarios like ensuring a batch job runs only once, electing a leader among several instances, or protecting a critical section of code that interacts with an external resource.
Principle of Operation
The core principle behind using advisory locks for distributed task coordination is straightforward: each distributed task or critical section is assigned a unique identifier (typically an integer). When an instance wants to execute this task, it attempts to acquire the advisory lock associated with that identifier.
- If the lock is available, the instance acquires it and proceeds with the task. This signifies that it is now the designated "owner" or "executor" of that task.
- If the lock is already held by another instance, the current instance can either wait for the lock to be released (blocking behavior) or immediately proceed with an alternative path (non-blocking behavior). The choice between blocking and non-blocking depends on the specific coordination requirement.
Once the task is completed, the instance explicitly releases the lock, making it available for other instances. This mechanism ensures that only one instance can successfully acquire and hold a specific lock at any given time, thereby coordinating access to shared resources or ensuring exclusive execution of a task.
Implementation Example: Ensuring a Batch Job Runs Once
Let's consider a common scenario: a daily batch job that should only be executed by a single instance, even if multiple application instances are scheduled to run it.
We'll use PHP with the pdo_pgsql extension, but the concept is easily transferable to other languages and ORMs.
<?php // Database connection details $dsn = 'pgsql:host=localhost;port=5432;dbname=mydatabase'; $user = 'myuser'; $password = 'mypassword'; // Unique identifier for our batch job's advisory lock // It's good practice to derive this from a constant or a meaningful string const BATCH_JOB_LOCK_ID = 123456; try { $pdo = new PDO($dsn, $user, $password, [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_CASE => PDO::CASE_NATURAL, ]); echo "Attempting to acquire lock for batch job...\n"; // Try to acquire a session-level advisory lock (non-blocking) // pg_try_advisory_lock(key) returns true if acquired, false otherwise $stmt = $pdo->prepare("SELECT pg_try_advisory_lock(:lock_id)"); $stmt->execute([':lock_id' => BATCH_JOB_LOCK_ID]); $result = $stmt->fetchColumn(); if ($result) { echo "Lock acquired successfully! Executing batch job...\n"; // Simulate batch job execution sleep(5); // The actual batch job logic would go here echo "Batch job completed.\n"; // Release the lock // pg_advisory_unlock(key) $stmt = $pdo->prepare("SELECT pg_advisory_unlock(:lock_id)"); $stmt->execute([':lock_id' => BATCH_JOB_LOCK_ID]); echo "Lock released.\n"; } else { echo "Could not acquire lock. Another instance is likely running the batch job. Skipping.\n"; } } catch (PDOException $e) { echo "Database error: " . $e->getMessage() . "\n"; exit(1); } ?>
Explanation:
- Unique Lock ID:
BATCH_JOB_LOCK_IDis an arbitrary integer that uniquely identifies this specific batch job across all instances. pg_try_advisory_lock(): This is the key function. It attempts to acquire a session-level advisory lock. If the lock is already held by another session (even from a different application instance), it immediately returnsfalsewithout blocking. This is crucial for avoiding deadlocks and allowing other instances to gracefully skip execution.- Critical Section: The code inside the
if ($result)block is the critical section – the part of the code that only one instance should execute. pg_advisory_unlock(): Once the critical section is complete (or if an error occurs and you want to explicitly free the lock),pg_advisory_unlock()is called to release the lock. It's vital to ensure locks are always released, whether through explicit calls or by the session ending.
Application Scenarios
- Leader Election: A set of instances can race to acquire a specific advisory lock. The one that succeeds becomes the leader. If the leader fails, its session eventually terminates, releasing the lock, and another instance can acquire it.
- Rate Limiting against External APIs: Before making a call to an external service with rate limits, an instance can acquire an advisory lock representing a "rate limit slot." This limits the number of concurrent calls.
- Distributed Cache Invalidation: When a cache needs to be invalidated, one instance can acquire a lock to perform the invalidation, preventing other instances from simultaneously attempting the same costly operation.
- Resource Reservation: Reserve a shared, limited resource for a specific period by holding an advisory lock.
Considerations and Best Practices
- Lock IDs: Use large, distinct integer values for lock IDs to avoid accidental collisions. Consider hashing strings or combining identifiers to generate unique lock IDs programmatically. PostgreSQL offers
pg_advisory_lock(bigint key1, bigint key2)for 128-bit keys, providing even more namespace. - Error Handling: Always handle database connection errors and ensure locks are released, even if the critical section fails.
- Session Termination: Session-level advisory locks are automatically released when the database session ends (e.g., application crashes, connection lost). This is a vital fail-safe mechanism.
- Blocking vs. Non-blocking: Choose
pg_try_advisory_lock()for non-blocking behavior (suitable for "run once" tasks where others should skip) andpg_advisory_lock()for blocking behavior (if you want instances to wait their turn). Be cautious with blocking locks in distributed systems to avoid deadlocks or long waits. - Monitoring: While advisory locks are not directly exposed in standard PostgreSQL monitoring views, you can query
pg_locksto see active advisory locks and identify potential issues. Look forlocktype = 'advisory'. - Granularity: Be mindful of the granularity of your locks. Overly broad locks can reduce concurrency, while overly fine-grained locks can increase complexity.
Conclusion
PostgreSQL advisory locks provide a powerful, lightweight, and often underutilized mechanism for distributed task coordination. By leveraging these application-level locks, developers can implement robust synchronization patterns directly within their database, avoiding the overhead and complexity of separate coordination services for many common scenarios. This approach offers simplicity, reliability, and efficiency, making it an excellent tool for orchestrating tasks and managing shared resources in distributed systems powered by PostgreSQL.

