remix-run/remix · warning · Error
MySQL migration lock was not held by the reserved connection
Error message
MySQL migration lock was not held by the reserved connection
What it means
After running migrations, the driver releases the named lock with SELECT release_lock(?) on the reserved connection and verifies the result. If release_lock does not report success (returns 0 because the lock was not held by this connection, or the query result is unexpected), this error is thrown during cleanup.
Source
Thrown at packages/data-table-mysql/src/lib/driver.ts:643
throw new Error('MySQL migration lock could not be acquired')
}
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 [unlockRows] = await connection.query('select release_lock(?) as `released`', [lockName])
if (!isRowsResult(unlockRows) || !toBooleanExists(unlockRows[0]?.released)) {
throw new Error('MySQL 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 isRowsResult(result: unknown): result is MysqlQueryRows {View on GitHub (pinned to 9696913134)
Solutions
- Keep migration runs shorter than connection/proxy idle timeouts
- Retry the migration run — the lock will be re-acquired cleanly on a fresh connection
- Check for external tooling or DBAs releasing named locks, and for pool settings that recycle connections mid-run
Example fix
// before await driver.runMigrations() // very long run; connection swapped mid-flight // after await driver.runMigrations() // after raising proxy idle_timeout / splitting migrations
Defensive patterns
Strategy: retry
Try / catch
try { await driver.runMigrations() } catch (e) { if (e instanceof Error && e.message.includes('was not held by the reserved connection')) { /* transient: connection swapped; retry run */ await driver.runMigrations() } else throw e } Prevention
- Keep migration runs under connection/proxy idle timeouts
- Split very large migration sets into smaller runs
When it happens
Trigger: The lock connection being severed/replaced mid-run (pool recycle, proxy idle timeout, MySQL restart) so the lock is no longer owned by that connection; the lock expiring via wait_timeout or another session force-releasing it; unexpected result shape from a proxy or fork (e.g. ProxySQL) rewriting lock functions.
Common situations: Long migrations exceeding idle timeouts behind a load balancer; kill-and-retry deploy tooling; server-side connection swaps. Note the driver catches this internally for cleanup, so it typically surfaces as a wrapped/secondary error rather than failing the migration result itself.
Related errors
- MySQL migration lock is already held by this database
- MySQL migration lock could not be acquired
- Unknown transaction token: + token.id
- MySQL database + method + () requires config-based construc
- MySQL database cannot + method + while transactions are op
AI-assisted analysis of remix-run/remix@9696913134 (2026-08-27).
Data as JSON: /api/errors/3fc4589afbfa9b5b.
Report an issue: GitHub.