npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

pg-advisory-lock

v2.2.0

Published

TypeScript library for PostgreSQL advisory locks

Readme

pg-advisory-lock

pg-advisory-lock coordinates exclusive work between application instances through PostgreSQL session-level advisory locks. It is useful when the protected work includes external I/O, non-transactional operations, or multiple database transactions.

Install

npm install pg-advisory-lock

Node.js 18.18 or newer and PostgreSQL 14 or newer are required. Use a direct connection or PgBouncer session pooling. PgBouncer transaction and statement pooling are unsupported.

Quick Start

Create one manager and reuse it for the lifetime of the application:

import { createAdvisoryLockManager } from "pg-advisory-lock"

const locks = createAdvisoryLockManager(
  "postgresql://user:pass@localhost/database",
)

await locks.withLock("db:migrate", async () => {
  await runMigrations()
})

// During graceful shutdown:
await locks.close()

withLock() waits until the lock is available, runs the callback, and releases the lock whether the callback returns or throws. It has no built-in acquisition timeout.

All applications using the same lock name in the same keyspace and connected to the same PostgreSQL database coordinate on the same advisory lock.

Locking Operations

Try Without Waiting

Use tryWithLock() when unavailable work should be skipped instead of queued:

const result = await locks.tryWithLock("reports:refresh", async () => {
  return await refreshReports()
})

if (!result.acquired) {
  console.log("Another worker is already refreshing reports")
}

When acquired, the result is { acquired: true, result }; otherwise it is { acquired: false }.

Reusable Helpers

Create a mutex when several call sites use the same logical name, or wrap a function that should always take a lock:

const mutex = locks.createMutex("search:index")
await mutex.withLock(rebuildSearchIndex)

const rebuildExclusively = locks.wrapWithLock(
  "search:index",
  rebuildSearchIndex,
)

An AdvisoryMutex provides withLock(), tryWithLock(), tryLock(), and wrapWithLock().

Manage a Lock Across Hooks

Most code should use withLock() or tryWithLock() so release is automatic. Use tryLock() when a framework only provides separate before/after hooks:

type LockContext = {
  unlock?: () => Promise<void>
}

async function before(context: LockContext) {
  context.unlock = await locks.tryLock("imports:run")
  if (!context.unlock) throw new Error("An import is already running")
}

async function after(context: LockContext) {
  await context.unlock?.()
}

A successful tryLock() reserves a connection until its idempotent unlock function is called. The integration must guarantee that the after hook runs even when the protected work fails.

A top-level tryLock() does not create a nesting context for later operations. Another top-level operation uses a different connection and will wait or fail on the same lock until unlock() runs.

Namespaces

Namespaces let different tenants or subsystems reuse the same logical lock names without coordinating with each other:

const { withLock } = locks.namespace("tenant-a")

await withLock("db:migrate", async () => {
  await migrateTenant()
})

locks.namespace("tenant-a") and locks.namespace("tenant-b") create derived keyspaces. The same name within one keyspace resolves to the same advisory key, while different namespaces normally resolve it to different keys.

Namespaces can be nested, and their order is significant:

const jobLocks = locks.namespace("tenant-a").namespace("jobs")

Derived keyspaces reuse the root manager's connection pool and lifecycle. Only the root manager exposes close().

Connections and Lifecycle

Passing a connection string or postgres.js options creates a postgres.js instance owned by the manager:

const locks = createAdvisoryLockManager({
  database: "app",
  host: "db.example.com",
  idle_timeout: 30,
  user: "app",
})

You can instead share an existing postgres.js instance with application queries:

import postgres from "postgres"
import { createAdvisoryLockManager } from "pg-advisory-lock"

const sql = postgres("postgresql://...")
const locks = createAdvisoryLockManager(sql)

Each top-level acquisition reserves one connection while waiting and for the duration of its callback, or until a manual lock is released. When application queries share the pool, lock waiters can exhaust it and prevent the current lock holder from finishing. Use a sufficiently large shared pool or a dedicated manager pool when contention is possible.

Closing the Manager

Derived keyspaces, mutexes, and wrapped functions share the root manager's lifecycle. close() stops new top-level acquisitions and waits for existing acquisitions to finish. It closes a postgres.js instance created by the manager, but not one supplied by the caller:

await locks.close()
await sql.end()

Calling it inside an active lock callback rejects to avoid waiting for that callback itself. Shutdown may wait indefinitely for a blocking acquisition, an unfinished callback, or a forgotten manual lock. Release every successful tryLock() before awaiting shutdown.

The root manager also implements the asynchronous disposal protocol, so integrations can use Symbol.asyncDispose instead of calling close() directly.

Reentrant and Concurrent Calls

Nested calls in the same active async context reuse its PostgreSQL session and are reentrant. The async context therefore acts as one owner, including for sibling calls:

await locks.withLock("resource", async () => {
  await Promise.all([
    locks.withLock("resource", task1),
    locks.withLock("resource", task2),
  ])
})

Both nested callbacks may overlap because they use the same lock-owning session. Nested calls are therefore not an additional in-process mutex. Independent top-level async contexts use separate sessions and coordinate through PostgreSQL.

Failure Model

If the lock-owning PostgreSQL session is lost, PostgreSQL releases the lock while its JavaScript callback may continue running. Another application instance can then acquire the same lock. The library therefore does not provide fencing or exactly-once execution. Use idempotency or fencing tokens when those guarantees are required.

Locks coordinate only within one PostgreSQL database, not across databases, clusters, primaries, or replicas.

Session-Level and Transaction-Level Locks

This library uses session-level locks because they can protect work outside a single database transaction. They remain held for the callback or until a manual lock is released.

When all protected work fits inside one transaction, PostgreSQL's pg_advisory_xact_lock may be simpler because it releases automatically at commit or rollback. Transaction-level locks and this manager have different lifecycles and are not interchangeable. The package does not promise common key derivation or automatic coordination between them.

Lock Key Derivation

The root keyspace converts each name to a signed 64-bit key with hashtextextended(name::text COLLATE "C", 0). Namespaces recursively derive the seed used to hash the name. Given H(value, seed) = hashtextextended(value COLLATE "C", seed):

locks.namespace("tenant-a").namespace("jobs")

uses H(name, H("jobs", H("tenant-a", 0))).

The C collation makes derivation independent of the database's default collation, and names remain case-sensitive. Different names or namespace chains can theoretically collide in the 64-bit keyspace. Treat derived numeric keys as an implementation detail. Do not persist them or use them as a cross-version interoperability contract.

Testing

Use pg-advisory-lock/testing to test code against the manager contract without connecting to PostgreSQL:

import { createTestAdvisoryLockManager } from "pg-advisory-lock/testing"

const locks = createTestAdvisoryLockManager()

If a test needs multiple managers connected to the same PostgreSQL lock space, create and pass shared state:

import {
  createTestAdvisoryLockManager,
  createTestAdvisoryLockState,
} from "pg-advisory-lock/testing"

const state = createTestAdvisoryLockState()
const locks1 = createTestAdvisoryLockManager({ state })
const locks2 = createTestAdvisoryLockManager({ state })

Acknowledgments

Originally inspired by advisory-lock.