Advisory Locks (One at a Time)

Problem / Context

You have a nightly job that sometimes starts twice from two workers, causing duplicated emails. You want only one to run at a time without adding new infrastructure.

Core Concept

Advisory locks are simple, app-controlled locks. Use them when you want only one session to run a piece of work at a time. They do not lock tables or rows for you. You pick a key; Postgres keeps a lock on that key.

Implementation (step by step SQL)

Examples:

Two types

Use a transaction lock for short critical sections. Use a session lock if the protected work spans multiple statements or transactions.

Core calls (single 64-bit key)

There are also two-key versions that take two 32-bit integers, for example: pg_advisory_lock(int, int). This is handy to “namespace” a lock, such as (tenant_id, resource_id).

Basic usage

SELECT pg_advisory_lock(12345);  -- acquire
-- do critical work
SELECT pg_advisory_unlock(12345); -- release

Transaction-scoped:

BEGIN;
SELECT pg_advisory_xact_lock(67890);
-- work
COMMIT; -- auto release

Non-blocking attempt:

SELECT pg_try_advisory_lock(42) AS got; -- run only if got = true

Hashing text names into keys

You can hash a string to a 64-bit key for the single-key functions:

SELECT pg_try_advisory_lock(hashtextextended('nightly-maintenance', 0));

For two-key functions (each key is 32-bit), use hashtext for both parts:

SELECT pg_try_advisory_lock(hashtext('tenant:7'), hashtext('refresh-report'));

Pattern: idempotent job runner

Your job runner can do a quick check like this:

WITH attempt AS (
  SELECT pg_try_advisory_lock(hashtext('job:daily-email')) AS ok
)
SELECT CASE WHEN ok THEN 'run' ELSE 'skip' END FROM attempt;

If it returns “skip”, another worker already holds the lock.

Pattern: per-account mutex

CREATE OR REPLACE FUNCTION process_account(_acct BIGINT)
RETURNS VOID
LANGUAGE plpgsql
AS $$
DECLARE
  locked BOOLEAN;
BEGIN
  SELECT pg_try_advisory_lock(_acct) INTO locked;
  IF NOT locked THEN
    RAISE NOTICE 'Account % busy; skip', _acct;
    RETURN;
  END IF;

  -- Ensure we always release the lock
  BEGIN
    -- critical work here
    NULL; -- placeholder
  EXCEPTION WHEN OTHERS THEN
    PERFORM pg_advisory_unlock(_acct);
    RAISE; -- bubble up the error
  END;

  PERFORM pg_advisory_unlock(_acct);
END;
$$;

Pitfalls (in plain words)

Tip: when skipping is okay, prefer pg_try_advisory_lock over waiting. You can also set statement_timeout to avoid waiting too long.

Monitoring locks

See who holds what:

SELECT pid,
       locktype,
       classid,
       objid,
       objsubid,
       pg_blocking_pids(pid) AS blocked_by
FROM pg_locks
WHERE locktype = 'advisory';

Count them:

SELECT COUNT(*) FROM pg_locks WHERE locktype = 'advisory';

Good practices

When not to use

Variations and Trade‑Offs

Recap (Short Summary)

Advisory locks are a small mutex toolkit built into Postgres. Use them on purpose, keep the locked work short, and release quickly. They prevent duplicate work without new infrastructure.

Optional Exercises

Summary (Cheat Sheet)

References