remix-run/remix · error · Error
Postgres migration lock is already held by this database
Error message
Postgres migration lock is already held by this database
What it means
withMigrationLock uses an AsyncLocalStorage-based store to detect re-entrancy: if migration code running under a lock calls withMigrationLock again on the same driver, this error is thrown. It prevents nested/duplicate advisory lock acquisition on one database connection pool.
Source
Thrown at packages/data-table-postgres/src/lib/driver.ts:377
}
/**
* Runs migration work on the postgres connection that owns the advisory lock.
*
* Lock acquisition waits up to 60 seconds and throws when the lock cannot
* be acquired. Re-entering this method from inside `run` throws instead of
* deadlocking, and a failed run destroys the reserved connection instead of
* returning it to the pool.
* @param name Logical migration lock name.
* @param run Migration work to run with a connection-bound driver.
* @returns The callback result.
*/
async withMigrationLock<result>(
name: string,
run: (driver: DatabaseDriver<'postgres'>) => Promise<result>,
): Promise<result> {
if (this.#migrationLockStore.getStore()) {
throw new Error('Postgres migration lock is already held by this database')
}
let waitForPreviousLock = this.#migrationLockQueue
let releaseQueue: () => void = () => undefined
this.#migrationLockQueue = new Promise((resolve) => {
releaseQueue = resolve
})
await waitForPreviousLock
try {
let releaseOnClose = false
let client: PostgresClient | PostgresPoolClient
if (isPostgresPool(this.#client)) {
client = await this.#client.connect()
releaseOnClose = true
} else {View on GitHub (pinned to 9696913134)
Solutions
- Remove the inner withMigrationLock call — migrations run inside a lock already; keep lock acquisition in one place only
- If composing utilities, pass a flag/skip the lock for nested invocations
- Run the nested migration logic directly (without lock) inside the outer locked callback
Example fix
// before
await driver.withMigrationLock('migrate', async (d) => {
await driver.withMigrationLock('inner', runInner) // throws
})
// after
await driver.withMigrationLock('migrate', async (d) => {
await runInner(d)
}) Defensive patterns
Strategy: validation
Validate before calling
// Only take the lock at the top level of your migration runner
if (alreadyInsideMigration) { return run(driver) } // skip nested lock
return driver.withMigrationLock(name, run) Prevention
- Acquire the migration lock in exactly one place
- Compose migration utilities by passing an unlocked run function
- Document which helper owns the lock
When it happens
Trigger: A migration's `run` callback calling driver.withMigrationLock again (directly or via a migrator that itself wraps migrations in a lock); composing two migration utilities that both take the lock; recursive migration runners sharing the driver.
Common situations: Nesting a custom migrate() helper that already runs under withMigrationLock inside another locked migration; upgrading the migrator so it now takes the lock while app code also does; test setups that chain locked migrations on the same driver.
Related errors
- MySQL migration lock is already held by this database
- MySQL migration lock could not be acquired
- MySQL migration lock was not held by the reserved connection
- Postgres migration lock could not be acquired
- Postgres migration lock was not held by the reserved connect
AI-assisted analysis of remix-run/remix@9696913134 (2026-08-27).
Data as JSON: /api/errors/817181b2a7852ec9.
Report an issue: GitHub.