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:
- Developer B pulled latest main (5 minutes)
- Created merge revision (2 minutes)
- Ran local tests (5 minutes)
- 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
-
Merge revisions are normal. They're not a mistake or workaround - they're how Alembic handles parallel development.
-
Empty migrations are valid. Merge revisions have no DDL operations, and that's correct. Their purpose is structural, not functional.
-
Communicate about migrations. A quick Slack message ("I'm about to create a migration") prevents 90% of conflicts.
-
Rebase before migrating. Creating migrations on stale branches guarantees conflicts. Rebase first.
-
CI catches conflicts early. Automated checks for multiple heads prevent conflicts from reaching production.
-
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 migrations3c7f1e2- Add CI check for multiple Alembic headsf8a9d1b- 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.