jd:/dev/blog_
· 4 min read

Fixing Alembic's Multiple Heads Problem with Git

Every team using Alembic with parallel branches hits the same wall. We built a library that uses git commit history to fix it.

Alembic multiple heads illustration

If you’ve used Alembic on a team with more than one developer, you’ve seen this error:

FAILED: Multiple head revisions are present for given argument 'head'

It means two migrations landed with the same down_revision, and Alembic doesn’t know which one comes first. The fix is always the same: someone manually edits the migration file to re-chain the revisions. At Mergify, with anywhere between 1 and 500 active migrations (depending on when we last squashed), this was happening multiple times a week. We got tired of it, so we built a library that makes it impossible.

How Alembic migrations work

Every Alembic migration file contains a revision (its own ID) and a down_revision (the ID of the migration it depends on). Together, they form a linked list:

A normal Alembic migration chain: rev_a → rev_b → rev_c → rev_d, linear

This works perfectly when one developer creates migrations at a time. Each new migration points to the previous head, and the chain stays linear.

What breaks with parallel branches

Real teams don’t work sequentially. Two developers create feature branches from the same main, and both generate a migration. Both migrations set their down_revision to the current head, rev_c:

Two branches both create migrations pointing to rev_c as down_revision, creating a fork with multiple heads

The first PR merges fine. The second one breaks because Alembic now has two heads: rev_d and rev_e both claim rev_c as their parent.

We use a merge queue, so this never breaks main directly. The queue detects the conflict and tells the developer to fix it. But it doesn’t fix it for them. Someone still has to stop what they’re doing, pull the latest main, update the down_revision by hand, and push again. The queue turns a production incident into a developer interruption, but the interruption still costs time.

Without a merge queue, it’s worse: the second PR merges, main breaks, and now everyone is blocked.

Alembic has a built-in alembic merge command for this. It creates a merge revision that joins the two heads. But it doesn’t solve the ordering problem: which migration runs first? If both touch the same table, the order matters, and alembic merge won’t pick it for you. You also end up with empty migration files that clutter the history, and you still need manual intervention every time. It patches the symptom without fixing the cause.

The insight: git already knows the order

We deploy migrations as soon as they merge. We never modify a migration after it’s merged. Those two constraints matter: they mean the migration chain is a single timeline, and the order is determined by when each migration file was first committed to main.

Git already tracks that. git log --reverse --diff-filter=A tells you exactly when each file was added to the repository, in order. That’s the chain. It works with squash merges, regular merges, and rebases, because all that matters is the final commit order on main.

The fix

We built alembic-git-revisions, an open source library that replaces the hardcoded down_revision with a function call:

from alembic_git_revisions import get_down_revision

revision = "5c9eb899ede0"
down_revision = get_down_revision(revision)

At runtime, get_down_revision builds the chain from git history and returns the correct parent. Two migrations pointing to the same down_revision? Doesn’t matter. Git commit order figures out the rest:

The fix: git commit order determines rev_d → rev_e, chain stays linear

The ordering stays linear regardless of how many branches merge in what order. To be clear: this solves the ordering problem (multiple heads), not semantic conflicts. If two developers both add a column with the same name, that’s a schema conflict and no tool can auto-resolve it. But the “who comes after whom” question? Git already answered it.

Existing migrations with hardcoded down_revision values keep working. The library detects which migrations use the old static style and which use the new dynamic resolution, then stitches them together into a single chain.

Setup

Install the library:

pip install alembic-git-revisions

Then update your Alembic migration template (script.py.mako) to import get_down_revision instead of hardcoding the revision. The package includes a reference template you can copy.

That’s it. Every alembic revision --autogenerate from that point on produces migrations that chain themselves.

What about Docker and CI?

Git history isn’t always available. Docker builds often use shallow clones or no repository at all. (If your CI pipeline is already under pressure, you don’t want migration resolution adding to the problem.) For those environments, the library can pre-generate a revision_chain.json file:

alembic-git-revisions /path/to/alembic/versions

This produces a JSON mapping of every revision to its parent. At runtime, the library checks for this file first and falls back to git only if it’s missing.

One month in

We’ve been running alembic-git-revisions for a month. Zero multiple heads errors. The manual re-chaining that used to interrupt developers multiple times a week just stopped happening.

The multiple heads problem is one of those things that’s small enough to ignore for a while and annoying enough to slow your team down over time. If you’re using Alembic with more than one developer, give it a try.

share:

Related posts