golang-migrate/migrate · warning
can't acquire lock
Error message
can't acquire lock
What it means
database.ErrLocked ("can't acquire lock") is returned by drivers' Lock() (and applyTableLock) when a migration lock is already held. Most drivers use an atomic isLocked flag; if CompareAndSwap(false, true) fails, someone (another goroutine, or the same instance) already locked this driver instance. It prevents concurrent migrations from racing.
Source
Thrown at database/driver.go:16
// Package database provides the Driver interface.
// All database drivers must implement this interface, register themselves,
// optionally provide a `WithInstance` function and pass the tests
// in package database/testing.
package database
import (
"fmt"
"io"
"sync"
iurl "github.com/golang-migrate/migrate/v4/internal/url"
)
var (
ErrLocked = fmt.Errorf("can't acquire lock")
ErrNotLocked = fmt.Errorf("can't unlock, as not currently locked")
)
const NilVersion int = -1
var driversMu sync.RWMutex
var drivers = make(map[string]Driver)
// Driver is the interface every database driver must implement.
//
// How to implement a database driver?
// 1. Implement this interface.
// 2. Optionally, add a function named `WithInstance`.
// This function should accept an existing DB instance and a Config{} struct
// and return a driver instance.
// 3. Add a test that calls database/testing.go:Test()
// 4. Add own tests for Open(), WithInstance() (when provided) and Close().
// All other functions are tested by tests in database/testing.View on GitHub (pinned to 01a9643f14)
Solutions
- Ensure every Lock has a matching Unlock, ideally via defer m.Unlock().
- Serialize migration attempts in-process with your own mutex, or use separate driver instances per goroutine backed by a database-level advisory/table lock.
- If a previous run leaked the lock, restart or recreate the driver instance (resets the in-memory flag), and check the DB lock table for stale rows.
Example fix
// before
m.Up() // Lock taken internally; retry loop calls again while held
// after
if err := m.Lock(); err != nil {
if errors.Is(err, database.ErrLocked) { log.Println("migration already running"); return }
return err
}
defer m.Unlock()
if err := m.Up(); err != nil && !errors.Is(err, migrate.ErrNoChange) { log.Fatal(err) } Defensive patterns
Strategy: try-catch
Try / catch
if err := m.Lock(); err != nil {
if errors.Is(err, database.ErrLocked) {
return fmt.Errorf("another migration is in progress")
}
return err
}
defer m.Unlock() Prevention
- Pair every Lock with a deferred Unlock immediately after a successful Lock.
- Run only one migration path per driver instance at a time; serialize with your own mutex if needed.
- Check stale lock rows (schema_lock) after crashed runs before retrying.
When it happens
Trigger: Calling m.Lock() twice on the same Migrate instance without Unlock; two goroutines sharing one driver instance both calling Lock; an earlier Lock whose deferred Unlock never ran (e.g. after a panic or early return).
Common situations: Concurrent migrate calls from the same process sharing a driver; leaked lock after a migration error skipped Unlock; running migrations from two workers on the same driver object in tests.
Related errors
- can't unlock, as not currently locked
- unable to obtain lock
- unable to release already released lock
- database is dirty
- database is dirty
AI-assisted analysis of golang-migrate/migrate@01a9643f14 (2026-09-02).
Data as JSON: /api/errors/383e1bc08f26e625.
Report an issue: GitHub.