>
dbwarden
/ tool scope / locking

Migration Locking
That Knows When to Stop.

Every schema change runs under a database-native lock, with holder diagnostics, heartbeat state, and a deliberate recovery path when a worker disappears.

One lock strategy per engine.

Most migration tools assume a single process running at a time, and fail silently when that assumption breaks. dbwarden enforces mutual exclusion at the database level, using whatever locking primitive the backend provides natively. The lock is part of the execution contract, not an optional wrapper around it.

PostgreSQL uses a session-scoped advisory lock, which is released automatically if the connection drops. MySQL and MariaDB use named user locks with a configurable timeout. SQLite uses BEGIN IMMEDIATE, which acquires a write lock on the database file for the entire migration run. ClickHouse, which has no session-scoped locks, uses a lease row with a fencing token and a configurable TTL.

Before anything executes, migrate acquires the lock and writes an observable status row. A second runner sees the status, reads the holder diagnostics, and fails immediately instead of racing the first migration. The status row records the holder identity, PID, host, execution ID, migration version, and health state, so an operator can tell who is running and whether they are still making progress.

$ dbwarden lock-status --database primary
Migration lock status
  State: RUNNING
  Health: HEALTHY
  Holder: deploy-worker-3 (PID 12345)
  Migration: V042
  Acquired: 2026-09-13T14:30:00Z
the lock is the contract
-- PostgreSQL: session advisory lock
-- Released on connection close, even after crash

-- SQLite: BEGIN IMMEDIATE
-- Write lock held for entire migration run

-- ClickHouse: lease with fencing token
-- TTL-based expiry, heartbeat renewal

The lock knows when the worker stops.

A lock that never expires is worse than no lock at all. If a migration process crashes or hangs, the lock stays held and every subsequent runner fails forever. dbwarden solves this with a heartbeat: the lock holder writes a timestamp at regular intervals, and the status checker compares the last heartbeat against the current time.

On native-lock engines (PostgreSQL, MySQL, SQLite), the heartbeat runs in a background thread and updates last_heartbeat_at on the status row. On ClickHouse, the heartbeat is part of the lease renewal. If the heartbeat stops, the lock status transitions from HEALTHY to STUCK, then to DEAD after a configurable timeout.

This gives operators a clear picture: HEALTHY means the migration is running and making progress. STUCK means the process may be hung. DEAD means the process is gone and the lock can be recovered. AVAILABLE means no lock is held. Each state has a clear remediation path.

$ dbwarden lock-status --database primary
Migration lock status
  State: STUCK
  Health: STUCK
  Last heartbeat: 2026-09-13T14:25:00Z
  Timeout: 120s
  Recommendation: Check process 12345 on deploy-worker-3
heartbeat states
HEALTHY  - running, making progress
STUCK    - no heartbeat for > timeout
DEAD     - no heartbeat for > 2x timeout
AVAILABLE - no lock held
FAILED   - lock acquisition failed
NEEDS_REVIEW - manual intervention required

Stale locks are diagnosed, not guessed.

When a lock is stuck or dead, the operator needs to decide whether to recover. dbwarden makes that decision explicit: unlock is a deliberate command that requires confirmation by default, records the action at audit level, and supports --force for automation.

The unlock command does not blindly release the lock. It checks the current status, confirms the holder is not still running, and only then releases. This prevents a race condition where two operators both think the lock is stale.

For distributed deployments, the official dbwarden-redis plugin provides a Redis-backed lock for application code and multiple replicas. The core database lock protects CLI commands; the Redis lock coordinates across application instances. They solve different problems and can be used independently or together.

$ dbwarden lock-status --database primary
Migration lock status
  State: DEAD
  Health: DEAD
  Last heartbeat: 2026-09-13T14:20:00Z

# Confirm recovery
$ dbwarden unlock --database primary

# Or force for automation
$ dbwarden unlock --database primary --force
install the optional plugin
$ dbwarden plugin add dbwarden-redis
View dbwarden-redis
What happens if a migration process crashes?

The lock is released when the connection closes. On PostgreSQL, the advisory lock is session-scoped and auto-releases. On SQLite, the journal cleanup releases the write lock. On ClickHouse, the lease expires after the TTL.

Can I check who holds the lock?

Yes. dbwarden lock-status shows the holder identity, PID, host, execution ID, and health state. dbwarden unlock is the explicit recovery command for stale locks.

Do I need Redis for multi-replica deployments?

The core database lock protects CLI commands. For application code and multiple replicas, the dbwarden-redis plugin provides a Redis-backed lock that coordinates across processes.

How does the heartbeat work?

A background thread writes a timestamp to the status row at regular intervals. If the heartbeat stops, the lock status transitions from HEALTHY to STUCK to DEAD, giving operators a clear picture of what is happening.

Choose dbwarden locking when

You run migrations in CI/CD pipelines or multi-developer environments where concurrent migration attempts are possible.

Choose something else when

You are the only developer, run migrations manually, and never have concurrent processes touching the database.

Lock architecture deep dive ClickHouse coordination profiles Locking overview