●CODE — Claude Code has moved on to v2.1.267. It was v2.1.263 yesterday, so four releases landed in the space of a single day●EFFORT — A new maxEffortLevel setting caps the effort level across every provider, Bedrock, Vertex and Foundry included. People can still choose something lower●CACHE — The largest part of this release is not a feature at all. More than a dozen fixes address cases where prompt cache reuse quietly broke●RESUME — Resuming a session or switching models with /model could rewrite the tool definitions, and the only visible symptom was a bill that crept upward●GATEWAY — v2.1.266 undoes a regression. Setups carrying CLAUDE_CODE_USE_GATEWAY were failing every request. The fix is the upgrade itself, not a config change●PLUGIN — --plugin-dir now accepts a folder of plugins, and a path containing a backslash can no longer slip past the containment check on macOS or Linux●CODE — Claude Code has moved on to v2.1.267. It was v2.1.263 yesterday, so four releases landed in the space of a single day●EFFORT — A new maxEffortLevel setting caps the effort level across every provider, Bedrock, Vertex and Foundry included. People can still choose something lower●CACHE — The largest part of this release is not a feature at all. More than a dozen fixes address cases where prompt cache reuse quietly broke●RESUME — Resuming a session or switching models with /model could rewrite the tool definitions, and the only visible symptom was a bill that crept upward●GATEWAY — v2.1.266 undoes a regression. Setups carrying CLAUDE_CODE_USE_GATEWAY were failing every request. The fix is the upgrade itself, not a config change●PLUGIN — --plugin-dir now accepts a folder of plugins, and a path containing a backslash can no longer slip past the containment check on macOS or Linux
Claude Code Multi-Agent Parallel Execution — Task Tool and SubAgent Patterns That Hold Up in Practice
Measured results from parallelizing Claude Code with the Task tool and SubAgents. Covers what to split and what never to split, a JSON extractor that survives real SubAgent output, and the arithmetic showing three agents had already hit the floor.
Back when I was running an article pipeline across four sites as an indie developer, watching a type check finish felt like a waste of a life. Tests could run. Linting could run. But only one at a time.
I parallelized the whole thing with the Task tool and SubAgents. The CI-equivalent stage went from 31 minutes to 9. That first run felt genuinely good.
And then it stopped improving. No matter how much I raised the degree of parallelism, nothing moved. Parallelism only touches the part of the work that can run independently. The rest is serial by nature, from the very beginning. This piece is mostly about where that line sits and how to find it.
What the Task Tool Actually Does
The Task tool is Claude Code's built-in mechanism for spawning new agents — SubAgents. Three behaviors matter.
Contexts are fully isolated. A SubAgent does not inherit the parent's conversation history. That is a design advantage, not a limitation. Each agent reasons in its own context, so running them together never contaminates one another's thinking.
Independent calls can be issued in a single message. Batch Task calls that have no dependency on each other into one message and their execution overlaps. If a call needs the previous result to compute its arguments, you have to wait. Whether something can be parallelized comes down to one question: are the arguments already known?
Results come back as strings. If you want structured data, instruct the SubAgent to return JSON — and validate it in the parent. Skip that validation and things break without a sound, as I'll show.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦A decision table for what to parallelize and what to keep serial — plus why the wrong call produces no errors and a broken artifact
✦Three JSON extraction strategies (greedy, lazy, brace-counting) measured against seven real SubAgent output shapes, and the extractor that passes all seven
✦The arithmetic showing a three-way fan-out had already reached its 743-second floor, and the order in which to shorten what remains
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
The first decision in multi-agent design isn't how to word the prompts. It's what to split. Here is the table I work from.
Nature of the work
Parallelize?
Why
Read-only analysis (type check, lint, tests)
Yes
No side effects, order is meaningless
Work on disjoint file sets (per-module review)
Yes
Write targets never overlap
Queries against different resources (DB, API, disk)
Yes
Latency is dominated by I/O waits
Writes to the same file
No
Last writer wins, one edit vanishes
Work depending on an earlier result (build then test)
No
Arguments aren't known until runtime
Mutating shared state (env vars, global config)
No
Readers observe an indeterminate state
Getting this wrong is nasty, because nothing fails. Every agent reports success. The aggregate looks clean. Only the artifact is broken.
I once handed two SubAgents an append operation on the same config file. Both replied that they had appended. Exactly one edit survived. I didn't notice until I walked back through the logs.
When writes are involved and you're unsure, fall back to serial. Protect correctness before speed.
A Three-Agent Parallel Setup
This is the shape I actually run in my indie developer repository after a large change. Three SubAgents, each given independent read-only work.
Agent 1: type check and lint
## SubAgent: TypeScript CheckerYou are a code quality checker. Run the following and return JSON only.No preamble, no explanation.1. Run `npx tsc --noEmit 2>&1` and collect errors2. Run `npx eslint src/ --format json 2>&1`3. Summarize counts and severityOutput:{"tsc_errors": 0, "eslint_errors": 0, "eslint_warnings": 0, "details": ""}If a command cannot run, return {"error": "reason it could not run"}.
Agent 2: tests and coverage
## SubAgent: Test Runner1. Run `npx jest --coverage --json 2>&1 | tail -1`2. Extract the coverage summary3. Collect failing test names as an arrayOutput:{"total_tests": 0, "passed": 0, "failed": 0, "coverage_pct": 0.0, "failed_tests": []}On timeout, return {"error": "timeout"}.
Agent 3: dependency audit
## SubAgent: Security Auditor1. Run `npm audit --json 2>&1`2. Extract high and moderate vulnerabilities3. Assemble the fix commandOutput:{"high_risk": 0, "moderate_risk": 0, "affected_packages": [], "fix_command": ""}
All three are read-only and their write targets never overlap. That is exactly why they can be launched together in one message.
Write the Aggregation Layer With Validation
This is where the implementation lives or dies. SubAgents do not always return the format you asked for. They add a sentence of preamble. They drop a field. They time out and return nothing at all.
The parent absorbs all of it.
// aggregate.ts — validate SubAgent output while aggregating ittype Ok<T> = { ok: true; name: string; data: T };type Fail = { ok: false; name: string; reason: string };type AgentResult<T> = Ok<T> | Fail;export function parseAgentOutput<T extends object>( name: string, raw: string, required: (keyof T)[],): AgentResult<T> { // Pull out the JSON payload even when prose surrounds it (extractJsonBlock is below) const block = extractJsonBlock(raw); if (!block) return { ok: false, name, reason: "no JSON block found" }; let data: T; try { data = JSON.parse(block) as T; } catch (e) { return { ok: false, name, reason: `parse failed: ${(e as Error).message}` }; } if ("error" in data) { return { ok: false, name, reason: String((data as Record<string, unknown>).error) }; } const missing = required.filter((k) => data[k] === undefined); if (missing.length > 0) { return { ok: false, name, reason: `missing fields: ${missing.join(", ")}` }; } return { ok: true, name, data };}export function aggregate(results: AgentResult<object>[]) { const succeeded = results.filter((r): r is Ok<object> => r.ok); const degraded = results.filter((r): r is Fail => !r.ok); return { // Abort only on total failure. Partial failure degrades and continues. verdict: degraded.length === results.length ? "aborted" : degraded.length > 0 ? "partial" : "complete", succeeded: succeeded.map((r) => r.name), degraded: degraded.map((r) => ({ name: r.name, reason: r.reason })), };}
Why extract the payload rather than parse the whole string? Because "return JSON only" still yields Understood. Here are the results: in front of it often enough to matter. A strict JSON.parse(raw) dies right there.
Why make verdict three-valued? Because killing the whole run when one SubAgent times out throws away two perfectly good results. Keeping a partial state lets a human say: type check and tests passed, re-run the audit only.
Measured: When the Extraction Regex Falls Over
Revising this article, I finally went back and questioned the one line I had been leaning on for months: raw.match(/\{[\s\S]*\}/), a greedy regex. It absorbs a preamble, certainly — but what happens when something follows the payload? I realized I had never checked while I was tracing why occasional parse failed entries were showing up in the aggregate log.
So I assembled seven shapes that actually come back and pushed each through three implementations. Node v22.23.2, measuring only whether JSON.parse succeeds on what was extracted.
Greedy: /\{[\s\S]*\}/ — first { to the last }
Lazy: /\{[\s\S]*?\}/ — first { to the first }
Brace-counting: walk to the matching }, tracking string literals and escapes
What the SubAgent returned
Greedy
Lazy
Brace-counting
A. JSON only
ok
ok
ok
B. Preamble sentence in front
ok
ok
ok
C. Trailing note containing {"extends": "next"}
fails
ok
ok
D. Wrapped in a code fence, followed by prose
ok
ok
ok
E. Contains a nested object ("meta": {...})
ok
fails
ok
F. Brace inside a string ("type {id: string} does not match")
ok
fails
ok
G. An error JSON, then a retry JSON after it
fails
ok
ok
Greedy fails two of seven — and both are shapes my own agent definitions produce routinely. C is the classic "JSON only" request answered with a helpful footnote. G is precisely how an agent that retries internally hands back its work.
Swapping greedy for lazy breaks two different cases instead. E appears the moment any raw npx jest --json output passes through, and F is literally the contents of Agent 1's details field: TypeScript error messages carry types like {id: string} all the time. The lazy fix, in other words, breaks the agent definitions I wrote myself.
Extracting JSON isn't a choice between greedy and lazy matching — it's a matter of counting braces. It took me an embarrassingly long detour through regex variants to land on that sentence.
// Return the first { through its matching }. Braces inside string literals don't count.export function extractJsonBlock(raw: string): string | null { let depth = 0; let start = -1; let inString = false; let escaped = false; for (let i = 0; i < raw.length; i++) { const c = raw[i]; if (inString) { if (escaped) { escaped = false; continue; } if (c === "\\") { escaped = true; continue; } if (c === '"') inString = false; continue; } if (c === '"') { inString = true; continue; } if (c === "{") { if (depth === 0) start = i; depth++; continue; } if (c === "}") { depth--; if (depth === 0 && start >= 0) return raw.slice(start, i + 1); } } return null;}
Twenty lines or so. I skipped those twenty lines for months in favor of one regex. The more you raise parallelism, the more aggregate input flows through this function — so a lossy extractor may have been quietly eating part of the speedup I thought I had won.
One note on case G: when two JSON objects follow one another, this implementation returns the first — the failure record. That lines up with having agents return {"error": "reason"} instead of retrying internally. Hide the retry inside the agent and the extractor has no way to know which object is the real answer.
Measured: What Shrank and What Didn't
Before and after, in seconds, median of three runs.
Stage
Serial
3-way parallel
Notes
Dependency install
212
212
Shared prelude, cannot shrink
Type check + lint
388
497
Bounded by the slowest branch (tests)
Tests + coverage
497
Dependency audit
171
Aggregate and write results
19
34
Cost of adding validation
Total
1,287 (~21 min)
743 (~12 min)
As measured on this repo
The 31 minutes I opened with was a cold-cache first run. Measured properly it's 21 minutes down to 12 — still a 42% cut.
Three Agents Had Already Reached the Floor
Reading that table as an exercise in parallelism surfaces something I missed at the time. The serial remainder is 212 seconds of install plus 34 of aggregation: 246 seconds. The parallel section can never drop below its longest single branch, 497 seconds. Add them and you get 743 — exactly the three-way measurement.
So raising parallelism to four or five doesn't move this pipeline by a second. It wasn't that adding agents failed to help; the floor had already been reached before I added them. The flat measurements were the expected result, and I only understood that once I did the arithmetic.
Configuration
Parallel section bounded by
Total
Kind
Serial
—
1,287 s (~21.4 min)
measured
3-way parallel (current)
tests, 497 s
743 s (~12.4 min)
measured
Four or more agents
tests, 497 s
743 s (unchanged)
arithmetic
Tests split in two
type check, 388 s
634 s (~10.6 min)
projected
Type check split as well
half the tests, 248 s
494 s (~8.2 min)
projected
Parallel section reduced to zero
—
246 s (~4.1 min)
theoretical floor
Those 246 serial seconds are 19.1% of the original 1,287, which caps the achievable speedup at 5.23x. The 1.73x I actually had was a third of the way there.
What I value about this calculation is that it names the next move in advance. Splitting the tests buys 109 seconds — and hands the critical path straight to the type check. Split the tests into four or eight while leaving the type check alone and nothing goes below 634. Before raising the degree of parallelism, be able to name which single branch holds the critical path. If you can't, the missing piece is measurement, not agents.
And the dependency install sits there at 212 seconds — 28% of the total, stubbornly serial. Until that shrinks, the ceiling on parallelism is already visible. Parallelizing a pipeline is, among other things, an exercise in making its serial sections impossible to ignore.
Three Traps I Fell Into
Three agents fighting over one disk.
When the execution environment was low on space, SubAgents unpacking caches at the same time failed partway through writes. Worse, the failure mode was polite: the command exited zero and returned truncated content. Before parallelizing, check free space and write permissions on the temp directory. It's an unglamorous step, and skipping it cost me half a day of hunting.
An agent reading a stale clone.
I reused a shallow clone as a scratch workspace, and one agent read a days-old snapshot and correctly reported "this feature isn't implemented." The agent wasn't lying. The input I gave it was old. Any SubAgent that makes a judgment call needs freshly refreshed input. Input freshness may well move outcomes further than prompt craft does.
Batching a check and the action that depends on it.
Put "run the gate, and if it passes, push" in one message and the push call can be issued before the gate's result is ever read. I shipped an artifact that hadn't passed, exactly this way. A verification step and the action that depends on it belong in separate turns. The same principle again: only calls whose arguments are already known may run together.
Common Anti-Patterns
Trying to hand context to a SubAgent.
# ❌ Based on our conversation so far, understand the project and refactor it# ✅ Refactor getUserById in src/api/users.ts:# - change the parameter to an object { id: string }# - throw NotFoundError instead of returning null when absent# - keep tests/api/users.test.ts passing
"Our conversation so far" does not exist for a SubAgent. Everything it needs goes in the task instruction.
Passing output downstream unvalidated.
Inserting parseAgentOutput alone eliminates most of the mysterious aggregation bugs — provided you swap in extractJsonBlock first. With a lossy extractor, validation keeps returning "no JSON block found," which is not the real reason anything failed.
Leaning on retries.
"If it fails, try again" is worse than it sounds, because failures stop being observable. Have the agent return {"error": "reason"} and let the parent decide. A structured failure is far easier to work with than a quiet success.
In Production: Review Assistance
The place I feel the benefit most is reviewing a change set.
change set finalized
│
├─ collect the list of changed files (serial — it defines the next arguments)
│
├─ [parallel] ──────────────────────
│ ├─ Agent B: review the logic changes
│ ├─ Agent C: check tests cover the change
│ └─ Agent D: check docs stay consistent
│
└─ parent: validate all three, aggregate by priority
Only the first step is serial, because that's where the file list handed to B, C, and D becomes known. I spent a while trying to parallelize it anyway. If the arguments aren't determined, it cannot run in parallel. A simple rule, but drawing it makes the hesitation disappear.
How to Start Small
You don't need a five-way fan-out on day one. The order I recommend:
Write out every step currently running serially and mark each with two flags: does it write anything, and does it consume an earlier result? Only the steps with no on both flags are candidates.
Then pick exactly two of them and run them together. Type check plus lint is usually the safest pair.
Next, add extractJsonBlock and validation equivalent to parseAgentOutput before you add a third. Defer validation and the mysterious inconsistencies begin the moment parallelism increases.
Finally, lay the three durations side by side and write down which one holds the critical path. There's no hurry to reach a fourth agent before you know that.
Start by putting your three current branches in a row and marking the longest one. It took me months to get around to marking mine. Thank you for reading.
Share
Thank You for Reading
Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.