{"record":{"id":"bd048806462ec64c","repo":"remix-run/remix","slug":"postgres-migration-lock-was-not-held-by-the-reserv","errorCode":null,"errorMessage":"Postgres migration lock was not held by the reserved connection","messagePattern":"Postgres migration lock was not held by the reserved connection","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/data-table-postgres/src/lib/driver.ts","lineNumber":610,"sourceCode":"  await client.query('set lock_timeout to default')\n\n  let outcome: { status: 'success'; value: result } | { status: 'failure'; error: unknown }\n\n  try {\n    outcome = { status: 'success', value: await run(driver) }\n  } catch (error) {\n    outcome = { status: 'failure', error }\n  }\n\n  let unlockFailed = false\n  let unlockError: unknown\n\n  try {\n    let result = await client.query('select pg_advisory_unlock(hashtext($1)) as \"released\"', [name])\n    let row = result.rows[0] as Record<string, unknown> | undefined\n\n    if (!toBooleanExists(row?.released)) {\n      throw new Error('Postgres migration lock was not held by the reserved connection')\n    }\n  } catch (error) {\n    unlockFailed = true\n    unlockError = error\n  }\n\n  if (outcome.status === 'failure') {\n    throw outcome.error\n  }\n\n  if (unlockFailed) {\n    throw unlockError\n  }\n\n  return outcome.value\n}\n\nfunction buildSetTransactionStatement(options: TransactionOptions): string {","sourceCodeStart":592,"sourceCodeEnd":628,"githubUrl":"https://github.com/remix-run/remix/blob/9696913134be3a4423513d2775f7b31d6917c049/packages/data-table-postgres/src/lib/driver.ts#L592-L628","documentation":"After running migrations, the driver releases the advisory lock with pg_advisory_unlock and expects the returned `released` flag to be true. If the unlock reports it did not hold the lock, this error is thrown, indicating lock state inconsistency on the reserved connection.","triggerScenarios":"The advisory lock was already released on that connection (double unlock, session reset); a superuser or script unlocked the lock mid-migration; connection pooler (pgbouncer in transaction mode) routing the unlock to a different server connection than the lock.","commonSituations":"PgBouncer transaction pooling breaking session-level advisory locks; a manual pg_advisory_unlock run during a stuck migration; prior migration crash leaving inconsistent session state that a pooler recycled.","solutions":["If using pgbouncer, enable session pooling (or bypass the pooler) for migration connections so advisory locks stay on one backend","Avoid manually releasing advisory locks while migrations run; verify with pg_locks before intervening","Retry the migration run; if persistent, restart the app/pooler to reset session state"],"exampleFix":"# pgbouncer.ini — migrations need session-level affinity\npool_mode = session","handlingStrategy":"validation","validationCode":"// with pgbouncer, verify session pooling for the migration role\nselect * from pg_locks where locktype = 'advisory';","typeGuard":null,"tryCatchPattern":"catch (e) { if (e instanceof Error && e.message.includes('was not held by the reserved connection')) { /* reset connections and retry once */ } throw e }","preventionTips":["Use session pooling (or direct connection) for migrations","Never manually release advisory locks mid-migration","Recycle the pool after lock anomalies"],"tags":["postgres","migration","advisory-lock","unlock"],"backgroundTag":"migration-lock-inconsistency","analyzedSha":"9696913134be3a4423513d2775f7b31d6917c049","analyzedAt":"2026-08-27T19:55:01.024Z","schemaVersion":2},"datasetVersion":"2026-08-28T00:17:15.603Z"}