Ecto Migration Lock Timeout in CI
Ecto acquires a database advisory lock so two migrators cannot run at once. If a previous migrator died holding it, or another run is migrating concurrently, ecto.migrate waits - and can time out. The stale-lock case is transient.
What this error means
mix ecto.migrate stalls while acquiring the migration lock and then fails with a connection/lock timeout. It commonly follows a cancelled or OOM-killed CI job that held the advisory lock when it died.
** (DBConnection.ConnectionError) tcp recv: closed / timed out while
waiting for the Ecto migration advisory lock to be releasedCommon causes
Stale advisory lock from a killed run
A prior migrator was cancelled or OOM-killed before releasing the advisory lock. The next run waits on it. Advisory locks are session-scoped, so a truly dead session’s lock clears once that connection is gone.
Concurrent migrations
Two pipelines migrating the same database at once contend for the lock; one waits while the other runs.
How to fix it
Retry once the stale session clears
A bounded retry usually succeeds once the dead migrator’s connection is reaped and its advisory lock releases.
for i in 1 2 3; do
mix ecto.migrate && break
echo "migration lock retry $i"; sleep 5
doneSerialize migrations to avoid real contention
- Use a CI concurrency group so only one migration job runs against a database at a time.
- Avoid two pipelines targeting the same database concurrently.
- Set a step timeout so a stuck lock surfaces fast instead of hanging.
Disable the lock only when safe
For a single-writer ephemeral CI database you can opt out of the migration lock, but never on a shared database.
# config/test.exs
config :my_app, MyApp.Repo, migration_lock: falseHow to prevent it
- Serialize migration jobs with a CI concurrency group.
- Set step timeouts so a stuck lock fails fast.
- Keep one migrator per database; rely on the advisory lock for safety.