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

  1. Ensure every Lock has a matching Unlock, ideally via defer m.Unlock().
  2. Serialize migration attempts in-process with your own mutex, or use separate driver instances per goroutine backed by a database-level advisory/table lock.
  3. 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

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


AI-assisted analysis of golang-migrate/migrate@01a9643f14 (2026-09-02). Data as JSON: /api/errors/383e1bc08f26e625. Report an issue: GitHub.