Use Advisory Locks
Use advisory locks for coordination — PostgreSQL Tutorial. Learn how to coordinate distributed processes with PostgreSQL advisory locks.
Focus: use advisory locks for coordination
You have a fleet of workers processing jobs from the same queue, and every once in a while two of them grab the same row and step on each other's toes. Row locks help, but what about a task that isn't tied to a row at all — like sending a daily digest or running a one-time migration? PostgreSQL advisory locks give you a lightweight, app-level coordination mechanism that doesn't bloat your schema or require a separate coordination service. In this lesson, you'll learn how to use advisory locks to coordinate distributed processes safely and efficiently.
The problem this lesson solves
Imagine you run a cron job on three servers, each firing the same maintenance script every midnight. Without coordination, all three might run the same vacuum or send the same email. Row-level locks don't help because there's no row to lock. You could use a pg_advisory_lock however, to ensure only one process runs at a time. Advisory locks solve a class of coordination problems that table locks can't: coordinating tasks that are not tied to a specific table or row.
Why care now? As your system grows from a single server to a cluster, you'll need a way to prevent duplicate work. Advisory locks are the PostgreSQL-native way to do this — no Redis, no Zookeeper, just SQL.
Core concept / mental model
Think of advisory locks as sticky notes on a shared whiteboard. Any process can write a note: "I'm working on task 42." Another process checks if the note exists before starting the same task. The whiteboard is the database, and the sticky note is a lock identified by a number (or text).
Advisory locks come in two flavors:
- Session-level: Held until you release it or the session ends. Like a sticky note stuck with strong tape — only you can remove it.
- Transaction-level: Held only for the duration of the current transaction. Like a sticky note that auto-destructs when the meeting ends.
There's also a shared vs. exclusive distinction:
- Exclusive lock: Only one session can hold it. Great for serializing tasks.
- Shared lock: Multiple sessions can hold it at once, but not concurrently with an exclusive lock. Useful for read-write coordination.
You can identify a lock by a bigint or two int4 keys, or by a text key (which is hashed to an int8 internally).
How it works step by step
Let's trace how advisory locks are used for coordination:
- Acquire the lock: A process calls
pg_advisory_lock(lock_key)(session-level) orpg_try_advisory_lock(lock_key)(non-blocking). If the lock is free, it's granted immediately. If not, the process waits (blocking) or gives up (try). - Do the work: Perform the task that must be serialized — sending emails, running migrations, etc.
- Release the lock: Call
pg_advisory_unlock(lock_key)or simply close the session. If you forget, the lock is released automatically when the session ends (or transaction ends for transaction-level locks).
For transaction-level locks, use pg_advisory_xact_lock(lock_key). It's released automatically at commit or rollback — no cleanup needed.
To avoid blocking forever, use pg_try_advisory_lock which returns true if acquired, false if not. This lets you implement a "if someone else is doing it, skip me" pattern.
Hands-on walkthrough
1. Basic session-level advisory lock
Open two connections (e.g., two psql sessions) and run:
-- Session 1
SELECT pg_advisory_lock(12345);
-- Returns void; lock acquired
-- Session 2
SELECT pg_try_advisory_lock(12345);
-- Returns false because Session 1 holds the lock
Now release in Session 1:
SELECT pg_advisory_unlock(12345);
Session 2 can now acquire:
SELECT pg_try_advisory_lock(12345);
-- Returns true
2. Transaction-level lock with auto-release
BEGIN;
SELECT pg_advisory_xact_lock(12345);
-- Do some work...
COMMIT; -- Lock released automatically
3. Non-blocking coordination pattern in a script
Here's a Python script using psycopg2 that only runs a job if no other process is already doing it:
import psycopg2
import time
conn = psycopg2.connect("dbname=mydb user=myuser")
cur = conn.cursor()
# Try to acquire a session-level advisory lock (non-blocking)
cur.execute("SELECT pg_try_advisory_lock(%s)", (42,))
acquired = cur.fetchone()[0]
if acquired:
print("Lock acquired. Running job...")
# Simulate work
time.sleep(5)
# Release the lock
cur.execute("SELECT pg_advisory_unlock(%s)", (42,))
conn.commit()
else:
print("Another process is running this job. Skipping.")
cur.close()
conn.close()
Expected output when run twice concurrently:
Lock acquired. Running job...
Another process is running this job. Skipping.
Compare options / when to choose what
| Mechanism | Scope | Blocking | Auto-release | Best for |
|---|---|---|---|---|
pg_advisory_lock |
Session | Yes | Session end | Long-running tasks, manual control |
pg_try_advisory_lock |
Session | No | Session end | Non-blocking check-then-run |
pg_advisory_xact_lock |
Transaction | Yes | Transaction end | Short tasks inside transactions |
pg_try_advisory_xact_lock |
Transaction | No | Transaction end | Non-blocking inside transactions |
Row-level SELECT FOR UPDATE |
Row | Yes | Transaction end | Protecting specific rows |
Table lock (LOCK TABLE) |
Table | Yes | Transaction end | Bulk operations on tables |
When to choose what?
- Use transaction-level locks when the work is atomic with the transaction — if the transaction fails, you don't want to keep the lock.
- Use session-level locks when you need to hold the lock across multiple transactions or while your app does complex logic.
- Use try locks when you don't want to wait — e.g., scheduled jobs that can skip if already running.
- Use row locks (
SELECT FOR UPDATE) when you are modifying specific rows — they coordinate at the row level, not just a global task.
Troubleshooting & edge cases
Lock not released after transaction
If you use pg_advisory_lock inside a transaction and forget to unlock, the lock is held until the session ends, not the transaction. This can cause deadlocks or stale locks. Use pg_advisory_xact_lock for transaction-scoped work.
Lock key collision
If you use two int4 keys, make sure the combination is unique. Using text keys is convenient but be aware that text keys are hashed to int8 — very rare collisions are possible (same as any hash).
Blocking forever
pg_advisory_lock blocks indefinitely. If the lock holder disappears (e.g., network drop), the lock is released when the server detects the session end. But if the holder is stuck, you may need to cancel it with pg_terminate_backend(pid).
Viewing locks
To see who holds advisory locks, query the pg_locks view:
SELECT pid, locktype, objid, granted
FROM pg_locks
WHERE locktype = 'advisory';
What you learned & what's next
You now understand how to use advisory locks for coordination in PostgreSQL. You learned to:
- Explain the core idea behind advisory locks: app-level coordination without table locks.
- Acquire session-level and transaction-level locks, both blocking and non-blocking.
- Apply a non-blocking pattern to prevent duplicate job execution.
- Choose between advisory locks, row locks, and table locks based on your scenario.
- Troubleshoot common issues like lock leaks and blocking.
Next step: In the next lesson, you'll dive into transaction isolation levels and see how advisory locks complement SERIALIZABLE transactions to build robust concurrent systems. You'll also learn how to combine advisory locks with LISTEN/NOTIFY for real-time coordination.
Now go ahead and experiment: write a small script that uses pg_try_advisory_lock to ensure only one instance of a cron job runs. You'll appreciate the power of this simple SQL feature.
Practice recap
Open two psql sessions. In session 1, acquire a session-level advisory lock on key 999. In session 2, try to acquire the same lock — it should return false. Release the lock in session 1, then retry in session 2 — it should return true. Then modify the example Python script to use a text key like 'email-blast' and run it twice in parallel to see the non-blocking behavior.
Common mistakes
- Using session-level advisory locks inside transactions without explicitly unlocking — the lock persists after the transaction ends, causing accidental blocking.
- Forgetting that
pg_advisory_lockblocks indefinitely by default; without a timeout or try variant, a stuck process can block your application forever. - Using text keys without considering that they are hashed to int8; if you rely on equality of text keys, be aware that hashing is not reversible and collisions are extremely rare but possible.
- Not checking the
pg_locksview when debugging "lock not released" issues; you may be looking at row locks when the problem is an advisory lock. - Using two separate
int4keys (e.g.,pg_advisory_lock(1, 2)) when a singlebigintkey would be simpler — mixing them can cause confusion and unintended blocking.
Variations
- Use
pg_advisory_xact_lockfor transaction-scoped coordination — especially useful when the work must roll back atomically with the lock release. - Utilize shared advisory locks (
pg_advisory_lock_shared) to allow multiple readers but block writers, similar toLOCK TABLE ... IN SHARE MODE. - Instead of a fixed numeric key, deriv the lock key from a business object's ID (e.g.,
hashtext('daily-digest')or'job:' || job_id) to coordinate per-item tasks.
Real-world use cases
- Ensuring only one application instance runs a scheduled database migration when multiple replicas are active (e.g., using
pg_try_advisory_lockat startup). - Serializing the delivery of time-sensitive notifications across a cluster of workers, preventing duplicate emails or push messages for the same user.
- Coordinating access to an external rate-limited API from multiple PostgreSQL-backed services by acquiring a session-level advisory lock before making requests.
Key takeaways
- Advisory locks provide app-level coordination without schema changes or external services.
- Session-level locks persist until released or session ends; transaction-level locks auto-release at commit/rollback.
- Use
pg_try_advisory_lockto implement non-blocking checks that skip tasks already in progress. - Lock keys are integers (or two integers, or text hashed to int8); choose a consistent naming scheme to avoid collisions.
- Advisory locks are complementary to row locks and table locks — each solves a different scope of coordination.
- Always inspect
pg_locksfor advisory locks when debugging locking issues.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.