Parallelization

By the Syncfusion DevTutorials team · Last updated

Run independent checks at the same time without breaking them: one field per branch, a timeout on every join, and a join policy that says what happens when a branch is missing.

Watch Three Branches Lose Each Other's Work

Before a release, three checks run on every package: security, licence and size. None needs another's result, so a graph runs them at the same time. That is the right shape: Lesson 1 showed independent work is where graphs win.

But this graph has two bugs. Run it and read the red lines.

Three branches, one field

First, all three branches write the same field, report. Each one replaced the one before it, so only the last report survived. That is a race.

Second, the security branch got stuck waiting on a slow server. The join waits for every branch, with no time limit. So the whole graph waited, and is still waiting.

Parallel work is fast only if it is safe. In this lesson you make it safe.

TL;DR

  • Parallelization runs independent branches at the same time. A fan-out edge starts them; a join node waits for them and merges.
  • What it buys: less waiting, and independent opinions.
  • Failure 1, the race: two branches write the same field, and one result is lost. Fix: one field per branch.
  • Failure 2, the hung join: one branch never finishes. Fix: a timeout on every join.
  • A join policy says what happens when a branch is missing: all, quorum or partial. Record which branches contributed.

A Real-World Example

Priya's team reviews every large pull request with three subagents at once: one for bugs, one for security, one for style. They all wrote their comments into one shared review.md.

Some days the bug reviewer's comments vanished. Both it and the style reviewer had read the file, added their part and saved it, a second apart. The second save wiped the first.

Other days the review never finished, because the security reviewer was stuck on a huge file.

The fix: each reviewer writes its own file (bugs.md, security.md, style.md). A join merges them. The join waits at most 10 minutes. If a reviewer is late, the join posts what it has and says which reviewer is missing.

What You Will Learn

  • How to fan work out to parallel branches
  • How to spot and fix a race on shared state
  • Why every join needs a timeout
  • How to choose a join policy: all, quorum or partial
  • Why the join must record who contributed

Fan Out, Then Join

In the graph file, one edge with a list of targets is a fan-out. Each branch then has an edge to the join.

edges:
  - { from: split, to: [security, licence, size] }
  - { from: security, to: join }
  - { from: licence, to: join }
  - { from: size, to: join }

Three branches start together. Two finish; one hangs. The join stops waiting at its timeout and goes on with what it has.

Running branches at the same time saves time: the graph takes as long as the slowest branch, not the sum of all of them.

Time: one by one vs side by side

const minutes = { security: 4, licence: 3, size: 1 };
const values = Object.values(minutes);

const oneByOne = values.reduce((a, b) => a + b, 0);
const sideBySide = Math.max(...values);

console.log("one by one:   " + oneByOne + " min");
console.log("side by side: " + sideBySide + " min");

Expected output:

one by one:   8 min
side by side: 4 min

Failure 1: The Race

A race happens when two branches write the same field at nearly the same time. Whichever finishes last wins. The other result is gone, with no error.

A race: three writers, one field

const state = {};
const finished = [
  { branch: "size",     at: 1, report: "size: 2 packages over 1 MB" },
  { branch: "licence",  at: 3, report: "licence: 1 GPL package" },
  { branch: "security", at: 4, report: "security: 0 known issues" },
];

for (const f of finished) state.report = f.report; // everyone writes "report"
console.log("final report: " + state.report);
console.log("lost: " + finished.slice(0, -1).map((f) => f.branch).join(", "));

Expected output:

final report: security: 0 known issues
lost: size, licence

The fix is the rule from Lesson 2: one writer per field. Each branch writes its own field. The join reads all of them and writes the merged result.

nodes:
  - { id: security, kind: agent, reads: [jobs], writes: [security_report], exit: { max_iterations: 3 } }
  - { id: licence, kind: agent, reads: [jobs], writes: [licence_report], exit: { max_iterations: 3 } }
  - { id: size, kind: function, reads: [jobs], writes: [size_report] }
  - { id: join, kind: join, timeout: 10m, policy: partial, reads: [security_report, licence_report, size_report], writes: [audit] }

Races also happen in files, not just in state fields. Two branches that edit the same file in the same folder can overwrite each other's changes. Give each branch that edits code its own copy of the repo (a git worktree). In Claude Code, set isolation: worktree on the subagent. In Cursor, a cloud agent works away from your local files. The Loop Engineering course covers worktrees.

Failure 2: The Hung Join

A join that waits for every branch, with no time limit, is only as reliable as its least reliable branch. One stuck network call, and the whole graph stops forever.

No race, but no timeout

Every join needs a timeout:. When the time is up, the join stops waiting, cancels the late branch and decides what to do.

A join with a timeout

const TIMEOUT = 10; // minutes
const branches = [
  { id: "security", at: Infinity }, // stuck
  { id: "licence",  at: 3 },
  { id: "size",     at: 1 },
];

const done = branches.filter((b) => b.at <= TIMEOUT).map((b) => b.id);
const late = branches.filter((b) => b.at > TIMEOUT).map((b) => b.id);
console.log("join at minute " + TIMEOUT);
console.log("contributed: " + done.join(", "));
console.log("cancelled: " + late.join(", "));

Expected output:

join at minute 10
contributed: licence, size
cancelled: security

Per-Branch Timeouts

A join timeout is the last line of defence. Each branch should also have its own timeout, shorter than the join's. Then a stuck branch is cancelled early and the join hears about it at once, instead of waiting for its own clock to run out.

nodes:
  - { id: security, kind: agent, timeout: 5m, reads: [jobs], writes: [security_report] }
  - { id: join, kind: join, timeout: 10m, policy: partial, reads: [security_report, licence_report, size_report], writes: [audit] }

Join Policies

When a branch is missing at the timeout, what should the join do? That is a design choice, and the graph file must say it.

All: fail if anything is missing. Quorum: go on with most of them. Partial: go on with whatever finished.

  • all: every branch must finish. If one is missing, the join fails on purpose and says which branch is missing. Use it when every part is required, such as a security check before a release.
  • quorum: more than half must finish. Use it for independent opinions, such as three reviewers voting.
  • partial: go on with whatever finished. Use it when some information is better than none, such as a nightly report.

Three policies, one missing branch

function join(policy, done, total) {
  const need = policy === "all" ? total : policy === "quorum" ? Math.floor(total / 2) + 1 : 1;
  return done >= need ? "proceed with " + done + " of " + total : "fail: only " + done + " of " + total;
}

for (const p of ["all", "quorum", "partial"]) console.log(p.padEnd(8) + join(p, 2, 3));

Expected output:

all     fail: only 2 of 3
quorum  proceed with 2 of 3
partial proceed with 2 of 3

Whatever the policy, the join must record which branches contributed. A report built from 2 of 3 checks should say so. Otherwise a reader assumes the security check ran, when it did not.

Failing on purpose is a good outcome. "The join timed out after 10 minutes; security did not finish" is a clear, quick message a person can act on. A graph that waits forever tells nobody anything.

Retries Without Double Work

A timed-out branch is often retried later. Make sure a retry does not do the work twice: check the state first, and skip what is already done. Agents call this being idempotent: running it again changes nothing that was already finished.

Merge at the Join

The join is the one place where the branch results meet. It reads each branch's own field, merges them, and records who took part.

A join that merges and records

const state = {
  licence_report: "1 GPL package",
  size_report: "2 packages over 1 MB",
  // security_report is missing: that branch timed out
};
const fields = ["security_report", "licence_report", "size_report"];

const have = fields.filter((f) => f in state);
const missing = fields.filter((f) => !(f in state));
const audit = have.map((f) => f.replace("_report", "") + ": " + state[f]).join("; ");
console.log("audit: " + audit);
console.log("missing: " + missing.join(", "));

Expected output:

audit: licence: 1 GPL package; size: 2 packages over 1 MB
missing: security_report

In the real tools, the branches are parallel subagents and the join is the main agent waiting for their results. Write the timeout and the policy in graph.md, and have each subagent write its result to its own file, such as audit/security.md.

A Worked Example: Translating a Service

QA Wolf describes a bigger use of this pattern: translating a service into another language. Each module is translated in a separate branch, at the same time. The check is not an opinion: the same inputs go through the old service and the new one, and the outputs must match. That is parallel branches feeding an external verifier, the strongest kind of join check there is.

Lab Exercise: Fix the Race and the Hung Join

Fix the race and the hung join

Open the graph file. Change it so that:

  1. Each branch writes its own field: security_report, licence_report and size_report.
  2. The join reads all three fields.
  3. The join has a timeout: of 30 minutes or less, for example timeout: 10m.
  4. Each branch has its own timeout:, shorter than the join's, for example timeout: 5m.
  5. The join has a policy:: all, quorum or partial.

Then press Check my graph. In each of the 3 versions, a different branch hangs. Your graph must have 0 races, the branch's own timeout must fire, and the join must proceed or fail as your policy says, and record who contributed.

Common mistakes

MistakeWrong ideaCorrect ideaWhat goes wrong
Shared output field"Each branch adds to the report."One field per branch; the join merges.The last branch wins and the others are lost.
Shared working folder"They edit different lines."One worktree per branch that edits code.Saves overwrite each other.
No join timeout"Every branch finishes in a few minutes."Every join gets a timeout.One stuck call hangs the whole graph.
No join policy"The join will figure it out."Say all, quorum or partial.Nobody knows if a half-finished result is acceptable.
Silent partial results"Two checks are nearly as good as three."Record who contributed and who is missing.Readers assume the missing check passed.

Exercises

Each exercise ends with a value on the last line. Press Check to compare it with the expected answer.

What to Remember

  • Fan out only independent work. The graph takes as long as its slowest branch.
  • One field per branch, and one worktree per branch that edits code. Shared outputs cause races.
  • Every join needs a timeout. One hung branch must not hang the graph.
  • Pick a join policy on purpose: all, quorum or partial.
  • Record which branches contributed, and which are missing.

Further Reading

Next: Orchestrator-Workers. An orchestrator starts a worker for every call site it finds, all at once, into a rate-limited API, and you cap its width, depth and spend.