{"record":{"id":"817181b2a7852ec9","repo":"remix-run/remix","slug":"postgres-migration-lock-is-already-held-by-this-da","errorCode":null,"errorMessage":"Postgres migration lock is already held by this database","messagePattern":"Postgres migration lock is already held by this database","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/data-table-postgres/src/lib/driver.ts","lineNumber":377,"sourceCode":"  }\n\n  /**\n   * Runs migration work on the postgres connection that owns the advisory lock.\n   *\n   * Lock acquisition waits up to 60 seconds and throws when the lock cannot\n   * be acquired. Re-entering this method from inside `run` throws instead of\n   * deadlocking, and a failed run destroys the reserved connection instead of\n   * returning it to the pool.\n   * @param name Logical migration lock name.\n   * @param run Migration work to run with a connection-bound driver.\n   * @returns The callback result.\n   */\n  async withMigrationLock<result>(\n    name: string,\n    run: (driver: DatabaseDriver<'postgres'>) => Promise<result>,\n  ): Promise<result> {\n    if (this.#migrationLockStore.getStore()) {\n      throw new Error('Postgres migration lock is already held by this database')\n    }\n\n    let waitForPreviousLock = this.#migrationLockQueue\n    let releaseQueue: () => void = () => undefined\n    this.#migrationLockQueue = new Promise((resolve) => {\n      releaseQueue = resolve\n    })\n\n    await waitForPreviousLock\n\n    try {\n      let releaseOnClose = false\n      let client: PostgresClient | PostgresPoolClient\n\n      if (isPostgresPool(this.#client)) {\n        client = await this.#client.connect()\n        releaseOnClose = true\n      } else {","sourceCodeStart":359,"sourceCodeEnd":395,"githubUrl":"https://github.com/remix-run/remix/blob/9696913134be3a4423513d2775f7b31d6917c049/packages/data-table-postgres/src/lib/driver.ts#L359-L395","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nawait driver.withMigrationLock('migrate', async (d) => {\n  await driver.withMigrationLock('inner', runInner) // throws\n})\n\n// after\nawait driver.withMigrationLock('migrate', async (d) => {\n  await runInner(d)\n})","handlingStrategy":"validation","validationCode":"// Only take the lock at the top level of your migration runner\nif (alreadyInsideMigration) { return run(driver) } // skip nested lock\nreturn driver.withMigrationLock(name, run)","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Acquire the migration lock in exactly one place","Compose migration utilities by passing an unlocked run function","Document which helper owns the lock"],"tags":["postgres","migration","lock","reentrancy"],"backgroundTag":"nested-migration-lock","analyzedSha":"9696913134be3a4423513d2775f7b31d6917c049","analyzedAt":"2026-08-27T19:55:01.024Z","schemaVersion":2},"datasetVersion":"2026-08-28T00:17:15.603Z"}