parallel() vs pipeline()
packages/core/src/runtime.ts
Both run work concurrently — the difference is the barrier. parallel() awaits everything before returning. pipeline() has no barrier: each item flows through all stages independently, so item A can be in stage 2 while item B is still in stage 1. Toggle below and watch the wall-clock.
What you're seeing
In parallel-then-parallel (two parallel() calls in sequence), stage 2 can't begin until the slowest stage-1 finishes — that red line is the barrier, and fast items sit idle against it. In pipeline(), each item's stage 2 starts the moment its own stage 1 is done. Wall-clock becomes the slowest single chain instead of the sum of slowest-per-stage.
// BARRIER: awaits everything. A throw becomes null (never rejects the group).
const parallel = (thunks) =>
Promise.all(thunks.map((t) => t().catch(() => null)));
// NO BARRIER: each item flows through all stages independently.
const pipeline = (items, ...stages) =>
Promise.all(items.map(async (item, index) => {
let prev = item;
try { for (const s of stages) prev = await s(prev, item, index); return prev; }
catch { return null; } // a thrown stage drops THAT item, keeps the rest
}));When to reach for which
- Default to
pipeline(). Most multi-stage work (find → verify, draft → critique) has no cross-item dependency, so a barrier just wastes the fast items' time. - Reach for a barrier (
parallel()) only when stage N genuinely needs all of stage N−1 — e.g. dedup/merge across the full result set, or an early-exit on a zero count. - Both swallow failures into
null. Filter with.filter(Boolean)before using the results.
Every concurrent branch still passes through the semaphore, so "100 thunks" never means "100 agents at once."