The first week I ran agents in parallel, I broke a build at midnight. Three Antigravity agents were each working on separate features. All three modified package.json. The last write erased what the other two had added — git history showed three clean PRs, but the actual package.json only carried one of the three new dependencies.
The real difficulty of running agents in parallel is that you can no longer trace, with the human eye, who touched what at which moment. This article is a structured account of the failures I hit while taking Antigravity parallelism seriously, organized by the kind of conflict, with reproductions and the fixes that hold up in production. If you are at the stage of believing "more agents will be faster," this is the moment to read it.
Five resources where parallel agents collide
After a week of operating in parallel, the conflicts converge onto roughly five hot spots:
- Writes to the same file —
package.json,tsconfig.json, lockfiles, shared schemas. - Commits to the same branch — when two or more agents diverge from the same parent, the late one falls into rebase hell.
- Shared caches and build artifacts —
.next/,node_modules/.cache/,dist/— one agent reads while another writes. - External API rate limits — three agents calling Gemini or Stripe with the same key cascade into 429s.
- Working directories and tempfiles —
/tmp/collisions, log file appends, SQLite WAL contention.
The first thing to internalize: do not design for "conflicts might happen." Design for conflicts will happen. The probability scales with the number of agents and time. Even a one-percent collision rate, run a hundred times a day, will fail at least once.
A reproducible minimum: the package.json race
Let me show the most common failure shape in a form you can paste into Node and run. Two parallel "read-then-write" operations easily produce a state where the last writer erases the earlier one.
// reproduce/package-race.ts
import { readFile, writeFile } from "node:fs/promises";
async function addDep(name: string, version: string) {
const pkg = JSON.parse(await readFile("package.json", "utf8"));
pkg.dependencies = pkg.dependencies ?? {};
pkg.dependencies[name] = version;
// Pretend the agent is "thinking"
await new Promise((r) => setTimeout(r, 200));
await writeFile("package.json", JSON.stringify(pkg, null, 2) + "\n");
}
await Promise.all([
addDep("zod", "^3.23.0"),
addDep("date-fns", "^3.6.0"),
addDep("nanoid", "^5.0.0"),
]);Run it and only the last write survives. The reads overlapped, so each writer overwrote a stale snapshot.
Parallel agent execution is structurally identical to this code. The only difference is that the "thinking" phase is seconds or minutes, not 200 milliseconds. The longer window makes collisions more likely, not less.
Fix 1: file locks — the most practical single-host choice
For agents running on a single machine, the simplest robust fix is an OS-level file lock. In Node, proper-lockfile is the de-facto choice and provides a lockfile-based mutual exclusion.
// utils/with-lock.ts
import lockfile from "proper-lockfile";
export async function withLock<T>(
path: string,
fn: () => Promise<T>,
): Promise<T> {
const release = await lockfile.lock(path, {
retries: { retries: 30, minTimeout: 200, maxTimeout: 2000 },
stale: 60_000,
});
try {
return await fn();
} finally {
await release();
}
}Wrap addDep and even at three-way parallelism the writes serialize cleanly:
await withLock("package.json", () => addDep("zod", "^3.23.0"));One caveat: always set a stale timeout. If an agent crashes mid-edit, the lock would otherwise sit forever and every subsequent run would hang. I keep it at 60 seconds, longer for jobs with known long-running steps.
Fix 2: optimistic locking — the right answer for branch conflicts
For conflicts at the branch level, locks are wrong. Optimistic locking — remember the version you read, retry if it changed by write time — is the practical fit, and Git is already an optimistic locking system. I encode the rules at the agent definition layer:
# antigravity/agents/branch-aware.yaml
name: branch-aware-coder
constraints:
- branch_per_task: true
- rebase_before_push: true
- max_rebase_attempts: 3
- on_conflict: "abort_and_report"branch_per_task is dumb and powerful. The agent works on a dedicated branch, attempts git pull --rebase origin main right before pushing, and aborts and reports if conflicts cannot be resolved cleanly.
I deliberately disable "auto-resolve conflicts." There was a release where one of my agents formatted around a conflict marker in package.json and shipped a broken build. Auto-resolution of merge conflicts is a mechanism for code to be merged without a human reading it. I keep it off, by default, on every project.
Fix 3: queues — for shared APIs and global rate limits
External API rate limits cannot be fixed by locks or branches. The right place to absorb them is outside the agents, in a queue. On my team, every Gemini API call goes through a thin Cloudflare Workers Queue.
// services/gemini-queue.ts
import { Hono } from "hono";
const app = new Hono<{ Bindings: { GEMINI_QUEUE: Queue<unknown> } }>();
app.post("/enqueue", async (c) => {
const job = await c.req.json();
await c.env.GEMINI_QUEUE.send(job);
return c.json({ status: "queued" });
});
export default app;
export async function queue(env: { GEMINI_QUEUE: Queue<unknown> }, batch: MessageBatch<{ prompt: string }>) {
for (const msg of batch.messages) {
await callGemini(msg.body.prompt);
msg.ack();
}
}The agents follow one rule: never call the API directly. The 429 rate becomes a property of the worker's configured concurrency, not the number of agents. The first time I doubled the agent count and saw the 429 rate stay flat, parallel operation finally felt scalable.
The three classic race conditions, in agent form
On top of the five hot spots, there is another layer: race conditions that look almost the same shape every time. The three I see repeatedly:
1. Read-Modify-Write
The most classic and the most common. The package.json race above is exactly this. A reads → B reads → A writes → B writes and B's write erases A's. Fix with a file lock, or with a single coordinator process: agents propose diffs, one process serializes the application of those diffs.
2. Check-Then-Act
A check (does the file exist?) and an action (write it) are separated by enough time for someone else to slip in between. Antigravity agents trying to create a "fresh" branch and colliding with one that already exists are this pattern. Fix by collapsing check and act into a single atomic operation: git rev-parse --verify new-branch && exit 1 || git checkout -b new-branch runs as one line and either succeeds atomically or fails fast.
3. ABA
An agent forks from branch A, the branch is recreated as a different "A," and the agent comes back assuming everything is the same. The fix is to remember the commit hash, not the branch name.
INITIAL_COMMIT=$(git rev-parse HEAD)
# ... agent work ...
git fetch origin main
git merge-base --is-ancestor "$INITIAL_COMMIT" origin/main \
|| { echo "Base branch was rewritten"; exit 1; }Choosing among three lock styles
To compress the patterns above:
- Pessimistic locks (file locks, mutexes) — frequent contention, short writes
- Optimistic locks (Git rebase) — rare contention, long writes
- Queues (ordering + backpressure) — shared resources, external APIs, global rate limits
My rule of thumb is simple: if the resource is a file, lock it; if it is a branch, rebase it; if it is an API, queue it. Two exceptions: SQLite, even in WAL mode, only allows one writer at a time and deserves a pessimistic lock; long-running migration jobs go to a dedicated job runner with single-worker concurrency.
Observability: noticing that contention happened
A conflict you do not detect becomes a production incident. The four signals my team watches:
- File lock acquisition time — warn at 1s, BLOCK at 10s.
- Rebase failure count — auto-pause an agent after 3 consecutive rebase failures.
- 429 ratio — if it crosses 5 percent, halve queue worker concurrency automatically.
- Job de-duplication — if the same
task_idis enqueued twice, drop one.
Surface these four on a Grafana or Cloudflare dashboard and you can review a few hours of data right after raising the parallelism level to confirm whether the new degree is safe.
A pre-flight checklist before turning the dial up
Closing with the checklist I run before bumping parallelism. Even a single pass through this list catches surprising risks.
- [ ] Are agent-written files protected by locks (or queues)?
- [ ] Does each agent work on its own branch and rebase before pushing?
- [ ] Are shared caches (
.next/,dist/) split per agent? - [ ] Is the external API key shared, and if so, is there a per-key rate budget?
- [ ] Can logs and traces be filtered by
agent_id?
Parallel agents do bring speed. But speed only pays off after the scaffolding is in place to catch what falls during a collision — that was the lesson from the night I had to rebuild package.json by hand. If your finger is currently hovering over the button to spawn a third agent, run this checklist once before you press it.