Resolving Alembic multiple head revisions errors

Run alembic heads --verbose, read both revisions, then alembic merge -m "merge" <rev1> <rev2> — the merge is an empty revision naming both as parents, so nothing is rewritten and any database already at either head upgrades cleanly. This is the error in detail from managing multiple heads and database branching.

Quick Answer

From the error to a single head Five steps. Listing the heads with verbose output gives the revision identifiers and the files they live in, which is what every subsequent step needs. Reading both revisions answers the only question a tool cannot: do they touch the same object in incompatible ways. Merging creates an empty revision naming both parents, which is a new file and leaves the originals untouched. Upgrading then applies whichever branch revision the database has not seen, followed by the merge, collapsing the two rows in alembic_version back to one. The final step is a CI assertion that heads returns a single line, which converts the next occurrence into a review comment. alembic heads --verbose identifiers and file paths never guess read both revision files do they touch the same object? the only manual step alembic merge -m "merge" a b a new, empty revision originals untouched alembic upgrade head missing branch, then the merge two rows collapse to one assert a single head in CI three lines of shell Only the second step needs judgement. The rest is mechanical, which is why the CI check at the end is worth more than the fix itself.

The error:

$ alembic upgrade head
FAILED: Multiple head revisions are present for given argument 'head';
please specify a specific target revision, '<branchname>@head' to narrow
to a specific head, or 'heads' for all heads

The fix:

# 1. Identify them — the verbose form gives the file paths too
$ alembic heads --verbose
Rev: 8f2c14a9b7d3 (head)
Parent: c3a1029ef455
Path: alembic/versions/8f2c14a9b7d3_add_order_status.py

Rev: 4b91de77c012 (head)
Parent: c3a1029ef455
Path: alembic/versions/4b91de77c012_add_customer_tier.py

# 2. Read both files. Do they touch the same table and column?

# 3. Merge — creates an empty revision with both as parents
$ alembic merge -m "merge order status and customer tier" 8f2c14a9b7d3 4b91de77c012
  Generating alembic/versions/e77a01bb4c19_merge_order_status_and_customer_tier.py

# 4. Upgrade
$ alembic upgrade head

The generated merge revision is empty by design:

revision = "e77a01bb4c19"
down_revision = ("8f2c14a9b7d3", "4b91de77c012")   # a tuple — both are parents


def upgrade() -> None:
    pass


def downgrade() -> None:
    pass

Do not delete either original revision. A merge is safe regardless of what any other environment has applied; deleting is not.

Execution Context & Async Workflow Integration

None of this changes for an async env.py — head resolution happens in the revision graph, before any connection is made. What does interact with async setups is where the error surfaces, because a deployment that runs migrations at application start-up hits it inside the container rather than in a pipeline step.

# Async — a start-up migration that fails clearly on multiple heads
from __future__ import annotations

import asyncio

from alembic import command
from alembic.config import Config
from alembic.script import ScriptDirectory


async def upgrade_to_head(url: str) -> None:
    config = Config("alembic.ini")
    config.set_main_option("sqlalchemy.url", url)

    script = ScriptDirectory.from_config(config)
    heads = script.get_heads()
    if len(heads) != 1:
        raise RuntimeError(
            f"refusing to migrate: {len(heads)} heads present ({', '.join(heads)}). "
            "Run: alembic merge -m 'merge' heads"
        )

    await asyncio.to_thread(command.upgrade, config, "head")

Checking the head count before running is worth the four lines: the message names the revisions and the command to fix them, where Alembic's own error explains the concept but not which revisions are involved.

Running migrations at start-up has its own problem in a multi-replica deployment — several containers racing to migrate the same database — which is why a dedicated job is preferable. The head check belongs in both.

Three different problems, three different messages Three states. Two heads in the revision files produce Multiple head revisions are present, and alembic heads shows both — this is the ordinary case and merging fixes it. Two rows in alembic_version mean the database itself is at multiple heads, usually because someone ran upgrade heads; alembic current shows both, and applying a merge revision collapses them. A version row naming a revision the files no longer contain produces a cannot-locate-revision error, which is a different problem entirely: a revision was deleted or rebased after being applied, and the fix is to restore the file rather than to merge anything. two heads in the files alembic heads shows two Multiple head revisions fix: merge two rows in alembic_version alembic current shows two someone ran upgrade heads fix: apply the merge a version row with no file cannot locate revision deleted or rebased after use fix: restore the file The third is the dangerous one, because the usual instinct — editing alembic_version by hand — makes the database inconsistent with every other environment.

Resolving Warnings, Errors & Common Mistakes

Error / WarningRoot CauseProduction Fix
Multiple head revisions are present for given argument 'head'Two revisions share a parent.alembic heads --verbose, read both, alembic merge.
Can't locate revision identified by '4b91de77c012'A revision was deleted or rebased after some database applied it.Restore the revision file from version control. Editing alembic_version is the last resort.
DuplicateColumn: column "status" of relation "orders" already exists after mergingBoth revisions added the same column; the merge fixed the graph, not the semantics.Edit one revision to remove the duplicate work — safe only before it reaches a shared environment.
alembic current shows two revisionsThe database itself is at two heads, usually from upgrade heads.Apply the merge revision; the two rows collapse to one.
The merge revision has three parentsalembic merge heads was run with three heads present.This is correct and supported; a merge may name any number of parents.
A merge revision contains schema operationsSomeone edited it.Merges should stay empty. Put the schema change in its own revision after the merge.

Advanced: Recovering From a Deleted or Rebased Revision

The genuinely awkward case is a database whose alembic_version names a revision the files no longer contain. It happens when a revision is rebased after being shared, or deleted in an attempt to resolve two heads.

The first move is always to restore the file, because that returns every environment to a consistent state:

# Find the revision in history and restore it
$ git log --all --diff-filter=D --name-only -- 'alembic/versions/*'
$ git checkout <commit>^ -- alembic/versions/4b91de77c012_add_customer_tier.py

If the revision is genuinely gone and cannot be recovered, the remaining option is to move the affected database to a revision that does exist. That is a manual, environment-by-environment operation and must be done knowing exactly what schema the database is in:

"""Move a database to a known revision WITHOUT running any migration.

`stamp` writes alembic_version and applies no DDL, so it is only correct when the
schema already matches the target revision. Verify that first — an incorrect stamp
makes every later migration operate on assumptions that do not hold.
"""
from alembic import command
from alembic.config import Config


def stamp_to(url: str, revision: str) -> None:
    config = Config("alembic.ini")
    config.set_main_option("sqlalchemy.url", url)
    command.stamp(config, revision)

Before stamping, confirm the schema really does match the target, using the same compare_metadata check described in using mapped_column instead of Column: an empty diff means the stamp is honest, and a non-empty one means stamping would record a lie that every subsequent migration builds on.

The prevention is a rule rather than a tool: a revision that has left your machine is immutable. Rebasing is fine on a feature branch nobody has pulled; after that, merging is the only safe operation. Enforcing it needs one CI check — reject a pull request that modifies or deletes a revision file already present on the main branch — which is a one-line git diff --name-status filter and removes the entire class of problem.

Merge adds; deleting removes something in use Two responses to the same two heads. Merging writes one new file whose down_revision names both leaves. Every existing revision keeps its identifier and its parent, so every database in every environment can still walk its own path forward — including a developer database sitting on one of the two branches. Deleting one of the revisions instead looks tidier and breaks any database that has already applied it: alembic_version names an identifier with no corresponding file, and every subsequent upgrade fails with a message about a revision that cannot be located. alembic merge adds one new file no existing revision changes every environment still upgrades safe in all cases delete one revision looks tidier in the file list breaks any database that applied it cannot locate revision, on every upgrade safe only if nothing ever applied it The asymmetry is the whole argument: merging is safe whatever the state of every other environment, and deleting requires knowing something you usually cannot verify.

Preventing Recurrence With One CI Check

Resolving two heads takes two minutes; discovering them during a deploy costs considerably more. A single-head assertion in CI moves the discovery to review time.

#!/usr/bin/env bash
# ci/check-migrations.sh — run on every pull request
set -euo pipefail

heads=$(alembic heads | grep -c '(head)' || true)
if [ "$heads" -ne 1 ]; then
  echo "ERROR: $heads heads present. Resolve with:"
  alembic heads --verbose
  echo "  alembic merge -m 'merge heads' heads"
  exit 1
fi

# A revision that has reached main is immutable: reject edits and deletions.
changed=$(git diff --name-status "origin/main...HEAD" -- alembic/versions/ \
          | grep -E '^(M|D)' || true)
if [ -n "$changed" ]; then
  echo "ERROR: existing revisions modified or deleted:"
  echo "$changed"
  exit 1
fi

Those two checks between them prevent both failure modes on this page: the second head that nobody noticed, and the rebase that breaks another environment. Neither needs a database, so they run in seconds on every pull request.

The one refinement worth adding for a larger team is to run the check against the merged result rather than the branch tip, since two branches can each have one head and produce two after merging. Most CI systems test the merge commit by default, which handles this — but a system configured to test the branch tip will pass both pull requests and fail on main, which is the exact scenario the check exists to prevent.

What the Two Heads Actually Cost

It is worth being clear that two heads are not damage. Nothing is corrupt, no revision is lost, and every database in every environment is still at a valid revision. What has happened is that the graph now has two leaves and head no longer names one thing, so Alembic refuses to guess.

That refusal is the right behaviour. Guessing would mean picking one branch and leaving the other unapplied, which produces a database that matches neither developer's expectations and no revision anyone can name. The error is Alembic declining to create that state.

The cost is therefore entirely in the interruption: a deploy stops, someone reads two revision files, runs one command, and the deploy proceeds. Two minutes, if it happens at review time. Considerably more if it happens during a release, which is the whole argument for the CI check below — the work is identical either way, and only the moment differs.

Frequently Asked Questions

Can I have more than two heads?

Yes, and alembic merge -m "merge" heads merges all of them into a single revision with as many parents as there were heads. That is supported and correct; the resulting revision file simply lists more identifiers in its down_revision tuple.

Does merging lose any of the work in either revision?

No. The merge revision is empty and both originals keep their identifiers, their contents and their parents. An upgrade after the merge applies whichever revisions the database has not yet seen, in dependency order.

Why did the merge succeed but the upgrade fail?

Because merging resolves the graph and not the semantics. If both revisions add the same column, the graph becomes valid and the second add_column then fails at execution. Read both revisions before merging — that check is the one part of the process no tool performs.

Is it safe to edit alembic_version directly?

It is a last resort, and only when you have verified that the schema matches the revision you are about to record. alembic stamp is the supported way to do the same thing, and it is still only correct when the schema already matches. An incorrect stamp is silent and every later migration inherits the mistake.