← Back

Alembic Migration Conflicts: Merge Revision Resolution

·database

Alembic Migration Conflicts: Merge Revision Resolution

Parallel feature development led to multiple migration branches in Alembic, causing "multiple heads detected" errors that blocked deployments. We resolved conflicts using merge revisions, enabling parallel development while maintaining a linear migration history.

The Problem: Multiple Migration Heads

Alembic tracks database schema history as a linked list of revisions. Each migration has a parent revision (down_revision), creating a linear history from database initialization to current state:

Initial → Migration A → Migration B → Migration C → Current

This works great until two developers create migrations simultaneously:

                 ┌─> Migration B1 (Feature 1)
                 │
Initial → Migration A
                 │
                 └─> Migration B2 (Feature 2)

Now there are two "heads" (B1 and B2), both claiming to be the latest migration. When you try to run alembic upgrade head, Alembic doesn't know which head to use and fails with:

alembic.util.exc.CommandError: Multiple heads are present; please resolve
  Revision b1: add_content_duo_tables
  Revision b2: add_analytics_events_table

This happened to us during parallel development of Content Duo and analytics features. Both branches added new tables. Both were ready to merge. Neither could deploy.

Conflict Scenario Before: Blocked Deployments

Development Timeline
┌──────────────────────────────────────────────┐
 Week 1: Developer A starts Content Duo      
 ├─ Creates migration: revision_123          
 └─ Base: revision_100                        
                                              
 Week 1: Developer B starts Analytics        
 ├─ Creates migration: revision_456          
 └─ Base: revision_100 (same parent!)        
                                              
 Week 2: Developer A merges PR to main       
 ├─ main now has: rev_100  rev_123          
 └─ Production deployed: rev_123             
                                              
 Week 2: Developer B tries to merge PR       
 ├─ PR has: rev_100  rev_456                
 ├─ main has: rev_100  rev_123              
 └─ CONFLICT: Two heads detected!            
                                              
 Result:                                      
 ├─ Developer B's PR blocked                 
 ├─ Deployment blocked                        
 ├─ Manual intervention required             
 └─ 2-4 hours to resolve                     
└──────────────────────────────────────────────┘

Migration History: Multiple Heads

Before Resolution (Broken)
┌──────────────────────────────────────────────┐
│                                              │
│     revision_100                             │
│     (base schema)                            │
│           │                                  │
│           ├──────────────┐                   │
│           │              │                   │
│           v              v                   │
│     revision_123   revision_456              │
│     (Content Duo)  (Analytics)               │
│                                              │
│     HEAD 1         HEAD 2                    │
│                                              │
│ Error: Multiple heads are present!           │
│ Alembic doesn't know which head to use       │
│                                              │
│ Symptoms:                                    │
│ ├─ alembic upgrade head → FAILS              │
│ ├─ alembic current → shows two heads         │
│ ├─ Deployment pipeline → BLOCKED            │
│ └─ Production stuck on revision_123          │
└──────────────────────────────────────────────┘

Solution: Merge Revisions

The fix is a "merge revision" - a special migration that declares both conflicting revisions as parents:

# migrations/versions/abc123_merge_revisions.py

"""Merge Content Duo and Analytics migrations

Revision ID: abc123
Revises: ('123', '456')  # Both parents!
Create Date: 2026-01-15 14:30:00

"""
from alembic import op
import sqlalchemy as sa

# revision identifiers
revision = 'abc123'
down_revision = ('123', '456')  # Tuple of both parent revisions
branch_labels = None
depends_on = None

def upgrade():
    # Empty upgrade function - no schema changes needed
    # This revision only exists to merge the two heads
    pass

def downgrade():
    # Empty downgrade function
    pass

The merge revision has no DDL operations. It exists solely to unify the two divergent branches into a single head.

Migration History: After Merge

After Resolution (Fixed)
┌──────────────────────────────────────────────┐
│                                              │
│     revision_100                             │
│     (base schema)                            │
│           │                                  │
│           ├──────────────┐                   │
│           │              │                   │
│           v              v                   │
│     revision_123   revision_456              │
│     (Content Duo)  (Analytics)               │
│           │              │                   │
│           └──────┬───────┘                   │
│                  │                           │
│                  v                           │
│            revision_abc123                   │
│            (merge revision)                  │
│                  │                           │
│                  v                           │
│              HEAD (single)                   │
│                                              │
│ Success: Linear history restored             │
│ Alembic can now upgrade to head              │
│                                              │
│ Result:                                      │
│ ├─ alembic upgrade head → SUCCEEDS           │
│ ├─ alembic current → shows abc123            │
│ ├─ Deployment pipeline → UNBLOCKED          │
│ └─ Production can deploy                     │
└──────────────────────────────────────────────┘

Step-by-Step Resolution Process

When we encountered multiple heads during the Content Duo launch, we followed this process:

1. Detect the Conflict

$ alembic heads
123abc (head) - Add Content Duo tables
456def (head) - Add analytics events table

$ alembic upgrade head
Error: Multiple heads are present; please resolve

2. Understand the Branch Structure

$ alembic history
123abc (head) - Add Content Duo tables
  ↓ (down_revision: 100xyz)
456def (head) - Add analytics events table
  ↓ (down_revision: 100xyz)
100xyz - Initial schema

Both migrations have the same parent (100xyz), creating a fork in the revision history.

3. Create a Merge Revision

$ alembic merge -m "Merge Content Duo and Analytics migrations" 123abc 456def

Generating merge revision abc123 from parents 123abc, 456def
  Generating /migrations/versions/abc123_merge.py ... done

Alembic generates a merge revision template:

"""Merge Content Duo and Analytics migrations

Revision ID: abc123
Revises: 123abc, 456def
Create Date: 2026-01-15 14:30:00
"""

revision = 'abc123'
down_revision = ('123abc', '456def')

def upgrade():
    pass

def downgrade():
    pass

4. Verify the Merge

$ alembic heads
abc123 (head) - Merge Content Duo and Analytics migrations

$ alembic history
abc123 (head) - Merge Content Duo and Analytics migrations
  ↓ (down_revision: 123abc, 456def)
123abc - Add Content Duo tables
  ↓ (down_revision: 100xyz)
456def - Add analytics events table
  ↓ (down_revision: 100xyz)
100xyz - Initial schema

Now there's a single head (abc123), and Alembic can upgrade successfully.

5. Test the Migration

# Test in local database
$ alembic upgrade head
INFO  [alembic.runtime.migration] Running upgrade 123abc, 456def -> abc123, Merge Content Duo and Analytics migrations

# Verify current state
$ alembic current
abc123 (head)

6. Commit and Deploy

$ git add migrations/versions/abc123_merge.py
$ git commit -m "Merge Alembic migration heads"
$ git push

The CI/CD pipeline can now run migrations successfully.

Real-World Resolution: Content Duo + Analytics

We encountered this exact scenario in January 2026. Two feature branches in parallel:

Branch A: Content Duo (Developer A)

  • Created 8 new tables for adaptive learning
  • Migration: 7d558b2_add_content_duo_tables.py
  • Parent: 100xyz

Branch B: Analytics Events (Developer B)

  • Created 2 new tables for event tracking
  • Migration: 489f4b1_add_analytics_events.py
  • Parent: 100xyz (same parent!)

Developer A merged first. When Developer B tried to merge, CI failed with "multiple heads" error.

Resolution took 15 minutes:

  1. Developer B pulled latest main (5 minutes)
  2. Created merge revision (2 minutes)
  3. Ran local tests (5 minutes)
  4. Committed and pushed (3 minutes)

Total impact: 15-minute delay for Developer B's merge, compared to the 2-4 hours we would have spent if we didn't understand merge revisions.

Common Mistakes When Creating Merge Revisions

Mistake 1: Forgetting to Specify Both Parents

Wrong:

down_revision = '123abc'  # Only one parent!

Correct:

down_revision = ('123abc', '456def')  # Tuple of both parents

If you only specify one parent, you haven't merged the heads - you've just created a third branch.

Mistake 2: Adding DDL Operations

Wrong:

def upgrade():
    op.create_table('users', ...)  # Don't add DDL in merge revision!

Correct:

def upgrade():
    pass  # Empty - merge revisions are structural only

Merge revisions should be empty. If you need schema changes, create a separate migration after the merge.

Mistake 3: Using alembic revision Instead of alembic merge

Wrong:

$ alembic revision -m "Merge migrations"

This creates a normal migration, not a merge revision. Use:

Correct:

$ alembic merge -m "Merge migrations" head1 head2

The alembic merge command automatically creates a revision with multiple parents.

Preventing Future Conflicts

While merge revisions resolve conflicts, prevention is better. We implemented three practices to reduce merge conflicts:

1. Coordinate Migration Timing

In Slack #engineering channel:

Developer A: "I'm creating a migration for Content Duo. Will merge by EOD."
Developer B: "Thanks for heads up. I'll wait until your PR merges before creating my analytics migration."

Simple communication prevents 90% of conflicts.

2. Rebase Before Creating Migrations

Before creating a migration, rebase your feature branch on latest main:

$ git checkout feature-branch
$ git rebase origin/main
$ alembic upgrade head  # Update local DB to latest schema
$ alembic revision -m "Add new tables"

This ensures your migration's parent is the latest production revision.

3. CI Check for Multiple Heads

We added a CI check that fails if multiple heads exist:

# .circleci/config.yml
jobs:
  test:
    steps:
      - checkout
      - run:
          name: Check for migration conflicts
          command: |
            # Count number of heads
            HEAD_COUNT=$(alembic heads | wc -l)
            if [ $HEAD_COUNT -gt 1 ]; then
              echo "Error: Multiple migration heads detected!"
              echo "Please create a merge revision before merging."
              alembic heads
              exit 1
            fi

This catches conflicts before they reach main branch.

Results

Before understanding merge revisions (3 months):

  • 4 migration conflicts
  • Average resolution time: 3 hours (including research, trial/error, and deployment delays)
  • 1 failed deployment (deployed wrong migration order)

After implementing merge revisions (3 months):

  • 2 migration conflicts (parallel development is common)
  • Average resolution time: 15 minutes (create merge revision, test, commit)
  • 0 failed deployments

Time saved: ~11 hours per quarter in migration conflict resolution

Alternative Approaches Considered

1. Serial Development (No Parallel Branches)

Approach: Only one developer creates migrations at a time.

Pros:

  • Zero migration conflicts
  • Always linear history

Cons:

  • Blocks parallel development
  • Slows down team velocity
  • Not realistic for teams >2 developers

Verdict: Rejected. Parallel development is essential for productivity.

2. Per-Feature Migration Namespaces

Approach: Use branch labels to isolate feature migrations.

revision = '123abc'
branch_labels = ('content_duo',)

Pros:

  • Allows parallel development
  • Explicit feature separation

Cons:

  • Complex to manage multiple branch labels
  • Still requires merge at deployment time
  • Alembic branch labels are poorly documented

Verdict: Rejected. Adds complexity without solving the fundamental issue.

3. Squash Migrations Before Merge

Approach: Squash multiple migrations into one before merging.

Pros:

  • Single migration per feature
  • Cleaner history

Cons:

  • Loses granular history
  • Doesn't prevent conflicts (still need merge revisions)
  • Harder to bisect issues

Verdict: Rejected. Squashing doesn't prevent conflicts.

Merge revisions are the standard Alembic solution and work well.

Key Lessons

  1. Merge revisions are normal. They're not a mistake or workaround - they're how Alembic handles parallel development.

  2. Empty migrations are valid. Merge revisions have no DDL operations, and that's correct. Their purpose is structural, not functional.

  3. Communicate about migrations. A quick Slack message ("I'm about to create a migration") prevents 90% of conflicts.

  4. Rebase before migrating. Creating migrations on stale branches guarantees conflicts. Rebase first.

  5. CI catches conflicts early. Automated checks for multiple heads prevent conflicts from reaching production.

  6. Document the process. New team members don't understand merge revisions. Include examples in your onboarding docs.

Implementation Commits

  • 89b04ad - Create merge revision for Content Duo and Analytics migrations
  • 3c7f1e2 - Add CI check for multiple Alembic heads
  • f8a9d1b - Document merge revision process in CONTRIBUTING.md

Conclusion

Alembic migration conflicts are an inevitable consequence of parallel development. Merge revisions are the standard solution, allowing multiple developers to create migrations simultaneously while maintaining a coherent deployment history.

Since learning to use merge revisions effectively, we've reduced migration conflict resolution time from 3 hours to 15 minutes and eliminated 100% of deployment failures caused by migration ordering issues. For teams practicing continuous deployment with multiple developers, understanding merge revisions is essential.

The next time you see "Multiple heads are present," don't panic. Create a merge revision, test it, and deploy confidently.