remix-run/remix · error · Error
Postgres migration lock was not held by the reserved connect
Error message
Postgres migration lock was not held by the reserved connection
What it means
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.
Source
Thrown at packages/data-table-postgres/src/lib/driver.ts:610
await client.query('set lock_timeout to default')
let outcome: { status: 'success'; value: result } | { status: 'failure'; error: unknown }
try {
outcome = { status: 'success', value: await run(driver) }
} catch (error) {
outcome = { status: 'failure', error }
}
let unlockFailed = false
let unlockError: unknown
try {
let result = await client.query('select pg_advisory_unlock(hashtext($1)) as "released"', [name])
let row = result.rows[0] as Record<string, unknown> | undefined
if (!toBooleanExists(row?.released)) {
throw new Error('Postgres migration lock was not held by the reserved connection')
}
} catch (error) {
unlockFailed = true
unlockError = error
}
if (outcome.status === 'failure') {
throw outcome.error
}
if (unlockFailed) {
throw unlockError
}
return outcome.value
}
function buildSetTransactionStatement(options: TransactionOptions): string {View on GitHub (pinned to 9696913134)
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
Example fix
# pgbouncer.ini — migrations need session-level affinity pool_mode = session
Defensive patterns
Strategy: validation
Validate before calling
// with pgbouncer, verify session pooling for the migration role select * from pg_locks where locktype = 'advisory';
Try / catch
catch (e) { if (e instanceof Error && e.message.includes('was not held by the reserved connection')) { /* reset connections and retry once */ } throw e } Prevention
- Use session pooling (or direct connection) for migrations
- Never manually release advisory locks mid-migration
- Recycle the pool after lock anomalies
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Postgres migration lock could not be acquired
- Postgres migration lock is already held by this database
- Route module ${routesFile} must export a named "routes" valu
- MySQL migration lock is already held by this database
- MySQL migration lock could not be acquired
AI-assisted analysis of remix-run/remix@9696913134 (2026-08-27).
Data as JSON: /api/errors/bd048806462ec64c.
Report an issue: GitHub.