{"record":{"id":"d8a78cc734e3da6c","repo":"kestra-io/kestra","slug":"migration-lock-is-held-by-another-process-another","errorCode":null,"errorMessage":"Migration lock is held by another process. Another instance may be running migrations. If this is unexpected, use 'kestra migrate unlock' to force-release the lock.","messagePattern":"Migration lock is held by another process\\. Another instance may be running migrations\\. If this is unexpected, use 'kestra migrate unlock' to force-release the lock\\.","errorType":"exception","errorClass":"MigrationLockedException","httpStatus":null,"severity":"error","filePath":"core/src/main/java/io/kestra/core/migration/MigrationRunner.java","lineNumber":115,"sourceCode":"        try {\n            executeMigrations();\n        } finally {\n            lock.release();\n        }\n        hasRun = true;\n    }\n\n    @Override\n    public void runOrFailIfLocked() throws MigrationLockedException, Exception {\n        if (hasRun) {\n            return;\n        }\n        if (scripts.isEmpty()) {\n            log.debug(\"No migration scripts found, skipping migration.\");\n            return;\n        }\n        if (!lock.tryAcquire()) {\n            throw new MigrationLockedException();\n        }\n        try {\n            executeMigrations();\n        } finally {\n            lock.release();\n        }\n        hasRun = true;\n    }\n\n    /**\n     * Re-synchronizes the stored checksum for a migration script that has already been applied.\n     *\n     * @param scriptId the migration script ID to repair\n     * @throws MigrationLockedException if another process holds the migration lock\n     * @throws Exception if the script is unknown, unapplied, unsupported, or the backend update fails\n     */\n    @Override\n    public void repairChecksum(final String scriptId) throws MigrationLockedException, Exception {","sourceCodeStart":97,"sourceCodeEnd":133,"githubUrl":"https://github.com/kestra-io/kestra/blob/823fada9274c4f9c251ea0a516460a4f7d958032/core/src/main/java/io/kestra/core/migration/MigrationRunner.java#L97-L133","documentation":"Thrown by MigrationRunner.runOrFailIfLocked() when a distributed migration lock (MigrationLock) cannot be acquired via tryAcquire(), meaning another process or Kestra node currently holds it. This is the guard that prevents two nodes from running schema migrations concurrently and corrupting the migration history table. The thrown MigrationLockedException carries no extra payload; the actionable guidance lives in the message: run 'kestra migrate unlock' to force-release a stale lock.","triggerScenarios":"Calling runOrFailIfLocked() while a second Kestra instance is already mid-migration; a previous migration crashed or was killed leaving the lock unreleased; the kestra migrate CLI was interrupted; on startup of a multi-node cluster where another node won the lock race.","commonSituations":"Multi-node deployments where nodes boot near-simultaneously; a migration that errored and the JVM was force-killed before the finally{lock.release()} ran; manual CLI migration runs left orphaned locks after a timeout; restart loops during a bad deploy.","solutions":["Confirm no other node/process is genuinely running migrations (check process list / other node logs).","Run 'kestra migrate unlock' to force-release the stale lock.","If using a JDBC backend, verify the lock row in the migration lock table is cleared after unlock.","Add a startup ordering strategy so only one node runs migrations on cluster bootstrap."],"exampleFix":"// before: two nodes race the lock\nnodeA: runOrFailIfLocked() // throws MigrationLockedException\n\n// after: release stale lock, then retry\n//   $ kestra migrate unlock\n// then restart the node, or call autoRun() again","handlingStrategy":"retry","validationCode":"// Before startup, check if the lock is already held\nboolean canProceed = migrationLock.tryAcquire();\nif (!canProceed) {\n    log.warn(\"Migration lock held; deferring startup or run 'kestra migrate unlock'.\");\n}\n// Release immediately if you only probed\nif (canProceed) migrationLock.release();","typeGuard":null,"tryCatchPattern":"try {\n    migrationRunner.runOrFailIfLocked();\n} catch (MigrationLockedException e) {\n    log.error(\"Could not acquire migration lock. Another node may be migrating. \"\n        + \"If stale, run: kestra migrate unlock\", e);\n    // Optionally: surface to operator / retry with backoff / fail startup\n    throw e;\n}","preventionTips":["Ensure only one node performs startup migrations in a multi-node cluster.","Always use 'kestra migrate unlock' after an unclean shutdown before restarting.","Monitor the migration lock table/entry for stale owners."],"tags":["migration","distributed-lock","startup","concurrency"],"backgroundTag":null,"analyzedSha":"823fada9274c4f9c251ea0a516460a4f7d958032","analyzedAt":"2026-08-14T06:15:17.947Z","schemaVersion":2},"datasetVersion":"2026-08-14T10:17:34.591Z"}