{"record":{"id":"3fc4589afbfa9b5b","repo":"remix-run/remix","slug":"mysql-migration-lock-was-not-held-by-the-reserved","errorCode":null,"errorMessage":"MySQL migration lock was not held by the reserved connection","messagePattern":"MySQL migration lock was not held by the reserved connection","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"warning","filePath":"packages/data-table-mysql/src/lib/driver.ts","lineNumber":643,"sourceCode":"    throw new Error('MySQL migration lock could not be acquired')\n  }\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 [unlockRows] = await connection.query('select release_lock(?) as `released`', [lockName])\n\n    if (!isRowsResult(unlockRows) || !toBooleanExists(unlockRows[0]?.released)) {\n      throw new Error('MySQL 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 isRowsResult(result: unknown): result is MysqlQueryRows {","sourceCodeStart":625,"sourceCodeEnd":661,"githubUrl":"https://github.com/remix-run/remix/blob/9696913134be3a4423513d2775f7b31d6917c049/packages/data-table-mysql/src/lib/driver.ts#L625-L661","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\nawait driver.runMigrations() // very long run; connection swapped mid-flight\n// after\nawait driver.runMigrations() // after raising proxy idle_timeout / splitting migrations","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"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 }","preventionTips":["Keep migration runs under connection/proxy idle timeouts","Split very large migration sets into smaller runs"],"tags":["data-table","mysql","migration","lock","cleanup"],"backgroundTag":"migration-lock-contention","analyzedSha":"9696913134be3a4423513d2775f7b31d6917c049","analyzedAt":"2026-08-27T19:55:01.024Z","schemaVersion":2},"datasetVersion":"2026-08-28T00:17:15.603Z"}