The hidden tax of parallel agents is a migration diamond
Six agents writing to one schema conflict in the database, not the code, and CI dies at "multiple heads."
Apollo Space Research
Apollo Space
Six agents finish six unrelated features on Tuesday. Six clean branches, six green test suites, six merges that all pass review. Then the seventh thing happens: the deploy refuses to run, the build prints a phrase no one wrote on purpose, multiple heads, and the database is now a fork. The code merged fine. The schema did not.
None of those six agents made a mistake. That’s the unsettling part.
Six agents writing to one schema conflict in the database, not the code, and CI dies at “multiple heads.” This post is about why that specific failure shows up the moment you run agents in parallel, why it has nothing to do with how good the agents are, and what we built so the conflict resolves itself before a human ever sees the word “heads.”
Why parallel agents are the whole point
Run one agent on a codebase and it works beautifully for an afternoon, then you watch it wait. It reads a file, thinks, writes, runs tests, waits for the suite, reads the result, moves on. One thread of attention through a backlog that has dozens of independent threads in it.
A real company codebase has more work than any single agent can hold at once. So you fan it out. One agent adds a field to the billing model, another builds a notifications table, a third indexes a slow query, a fourth backfills a column, and they all run at the same time, each in its own isolated copy of the repository. The throughput is the reason you did this. Parallelism is not a nice-to-have here, it’s the entire economic case for building software with a fleet instead of a single hand.
And for the code, isolation works. Two agents editing two different files never collide. Even two agents editing the same file usually merge cleanly, because git was built by people who expected exactly this and gave us three-way merges to handle it. The version-control layer is genuinely good at letting many writers touch one tree.
The trouble is that the codebase is not the only thing those agents are writing to.
The naive version: treat a migration like any other file
Here is the assumption that fails, and it’s so reasonable that almost everyone makes it first. A migration is a file. It lives in the repo, it’s tracked by git, it gets reviewed in a pull request like everything else. So if git can merge two agents’ code changes, surely it can merge two agents’ migrations.
It can merge the files. That’s the trap.
A migration isn’t really a file. It’s an instruction to change a thing that lives outside the repository, the database, and that thing has a single, shared, current state. Every migration is written as a step from the state the author last saw. The agent adding a billing field wrote its migration on top of state version A. The agent building the notifications table also wrote its migration on top of state version A, because that’s the state it last saw too. Neither one was wrong. They were both looking at the same starting point, because they started at the same time.
Now both migrations land. Git merges the two files without complaint, they don’t even touch the same directory in a way it cares about. But the migration tool reads them and finds two different instructions that both claim to be “the next step after A.” There is no single next state anymore. There are two. The history forked.
That fork has a name in every migration framework, and it’s the word that stops the deploy: multiple heads. The tool literally cannot decide which branch of the schema is the real one, so it refuses to run any of them. Your code is merged, your tests were green, and your database is now a tree with two tips and no trunk.
The dumb version of the fix is the one a tired engineer reaches for at 6pm: open both migration files, hand-edit one so it points at the other instead of at A, re-stitch the chain into a line, and pray you got the order right. It works. It also means a human is now doing, by hand, the one thing the whole parallel fleet existed to avoid, serializing the work after the fact.
Why “just lock the database” doesn’t save you
The next instinct is to remove the race entirely. If two agents can’t both branch from state A, then don’t let them. Make every agent that wants to write a migration take a lock, generate its migration against the truly-current head, release the lock, and let the next one go.
It resolves the conflict. It also quietly deletes the reason you went parallel.
A migration lock is a single lane that every schema change has to pass through one at a time. The moment your fleet does anything database-shaped, and a growing product touches the schema constantly, your six parallel agents line up single file at that lane and wait their turn. You bought a fleet and got a queue. Worse, the lock has to be held across the agent’s whole think-and-write cycle, which is long and unpredictable, so the lane isn’t just narrow, it’s slow. The throughput you were paying for drains out through the one place you forced everything to be sequential.
A lock doesn’t remove the conflict. It just charges you the conflict up front, on every change, whether or not one would have happened.
So you’re left with two bad options that are really the same option wearing different clothes. Serialize before the work with a lock and pay the tax on every migration. Or serialize after the work by hand and pay it in human attention whenever the fork actually happens. Both of them are a tax on parallelism. Both of them are the thing the fleet was supposed to make obsolete.
The conflict is real. Pretending it away breaks correctness; locking it away breaks throughput. The only honest move is to keep the parallelism and keep the schema linear, which means resolving the fork instead of preventing it.
Our way: make the diamond resolve itself
The shape of the problem is a diamond. Many migrations start from one shared state at the top, fan out as agents work in parallel, and then have to converge back into one linear history at the bottom. The top is divergence, that’s the point of the fleet. The bottom is convergence, that’s what the database requires. The hard part is the corner where they meet, and that corner is exactly the work we automated.
The key idea is simple: a fork in the migration history is not an error to escalate. It’s a routine event to resolve, and resolving it is mechanical. Six agents writing to one schema conflict in the database, not the code, and the conflict has a known shape, so it has a known repair.
When two migrations both claim to follow state A, there’s almost always a true answer about how to make them sequential, pick an order, rewrite the second one so its starting point is the first one’s ending point, and the two tips collapse back into a single trunk. Migration tools have shipped a command for this for years; the framework can stitch two heads into one. What’s been missing isn’t the command. It’s something on the hook to run it the instant a fork appears, instead of a human discovering it three hours later when the deploy dies.
So we put an agent on that hook.
When the fleet’s branches come together, a detector checks one thing: does the migration history have exactly one head? If it does, nothing happens, the common case stays free, because most merges don’t touch the schema at all. If it finds two, it doesn’t stop the line and page a person. It hands the fork to an agent whose only job is to make it linear again: read both migrations, decide which one comes first, re-parent the later one onto the earlier one’s end state, regenerate the chain, and re-run the suite against the stitched history to prove the result still applies cleanly.
Apollo is built so that this resolution is a step in the pipeline, not an incident. The agents stay parallel, nobody waits at a lock to write a migration. The schema stays linear, the database never sees two heads reach deploy. The diamond opens at the top because that’s where the throughput is, and closes at the bottom because that’s what correctness demands, and the corner where it closes is handled by something that runs in seconds and never gets tired of doing it.
Notice what this buys that a lock never could. A lock pays the serialization cost on every migration, including the overwhelming majority that would never have conflicted. The diamond pays it only on the actual forks, the rare moment two agents really did start from the same state and really did both touch the schema. The common case is free. The expensive case is automated. You stop taxing the fleet for a conflict that, most days, didn’t happen.
The turn: the queue was the tax all along
There’s a version of this where you never notice the problem, and it’s the saddest version. You run one agent, it’s slow but it never forks the schema, and you conclude that parallel agents “have a migration problem.” They don’t. Sequential work has a throughput problem, and parallelism is the cure, the migration fork is just the bill that cure quietly hands you, and most teams pay it without ever reading the line item.
The teams that feel this most aren’t the ones with bad agents. They’re the ones whose agents are good enough to actually run six at once, the better your fleet, the harder this corner hits, because a fleet that ships more schema changes forks more often. The failure scales with your success. That’s the tell that it’s structural, not a bug in anyone’s prompt.
What we ended up building wasn’t really a database trick. It was a refusal to accept the false trade, parallel-but-incorrect on one side, correct-but-serial on the other, and an insistence that the conflict is small enough to resolve mechanically the moment it appears. Six agents writing to one schema conflict in the database, not the code. So we taught the pipeline to close the diamond instead of asking a person to.
The part that doesn’t come out of a migration framework is the judgment that a forked history is a normal Tuesday, not a crisis, that the right response to “multiple heads” is a quiet automatic re-parent, not a slumped engineer with two files open and a deadline tonight. That judgment is the thing a fleet has to carry for the people running it, because the whole promise of a fleet is that you get the throughput of many hands without inheriting the coordination headache of many hands.
That’s what we’re building at Apollo Space: an operating system where running a hundred agents in parallel feels like running one, because the seams between them, the forks, the locks, the merges that the database notices and git doesn’t, close themselves before they reach you. If you’ve ever lost a Friday to the words multiple heads on a deploy that everything else said was green, you already know which part of the job a system should take off your plate first.
Apollo runs your company's repetitive ops so your team doesn't.
Join the waitlist for early access, founding-user pricing, and a front-row seat as we ship.
Join the waitlistAn orchestrator that can't survive its own crash isn't one
A crash that erases the orchestrator's reasoning loses the one thing you can't rebuild.
EngineeringPut a deterministic gate in front of your smartest reviewer
The cheapest defect-catch is a dumb script that checks two merged branches still boot before any judgment.
EngineeringEvery authenticated page must live inside the shell
A page that "works" but drops the navigation around it is a regression even when every test is green.