Concurrent work needs an owner and a bound
Choose how many operations may run at once. Give child work an owner that can stop it. A completed caller must not silently leave its work running.¶
Bound collection processing before running it
A traversal processes each input in a collection. An Effect describes work before execution.¶
Effect.forEach describes one Effect per input and collects results in input order.
Its concurrency option bounds active operations.
The input collection can be larger than that bound.
The checked fixture measures active work with a Ref, an Effect value that stores mutable state.¶
const activity = yield* Ref.make({ active: 0, maximum: 0 });
const input = Array.from({ length: options.count }, (_, index) => index);
const results = yield* Effect.forEach(
input,
(index) =>
Effect.acquireUseRelease(
Ref.update(activity, (state) => ({
active: state.active + 1,
maximum: Math.max(state.maximum, state.active + 1),
})),
() => Effect.yieldNow.pipe(Effect.andThen(Effect.succeed(index))),
() =>
Ref.update(activity, (state) => ({
...state,
active: state.active - 1,
}))
),
{ concurrency: options.concurrency }
);
return { ...(yield* Ref.get(activity)), results };
}The generated test varies both the input count and the bound. It requires completed work, no remaining activity, input-order results, and a peak that does not exceed the supplied bound.¶
const Traversal = Schema.Struct({
concurrency: Schema.Literals([1, 2, 3, 4]),
count: Schema.Literals([1, 2, 3, 4, 5, 6, 7, 8]),
});
it.effect.prop(
"traversal never exceeds its supplied bound and preserves input order",
{ scenario: Arbitrary.schema(Traversal) },
({ scenario }) =>
Effect.gen(function* traversalBound() {
const observed = yield* boundedTraversal(scenario);
expect(observed.maximum).toBeGreaterThan(0);
expect(observed.maximum).toBeLessThanOrEqual(scenario.concurrency);
expect(observed.active).toBe(0);
expect(observed.results).toEqual(
Array.from({ length: scenario.count }, (_, index) => index)
);
})
);
The expectation uses the supplied bound. It is independent of the traversal’s scheduling algorithm.¶
A planted violation replaced the bound with unbounded.
The property failed with seven active operations against a bound of one. The restored implementation passes.
Measure production limits separately from these generated test counts.¶
Stop child work with its Scope
The sandbox writes queued input to a child process through a scoped fiber: child work that stops when its owning Scope20 closes. A fiber is an independently running Effect. A Scope owns cleanup.¶
const outbox = yield* Queue.unbounded<Uint8Array, Cause.Done>();
yield* Stream.fromQueue(outbox).pipe(
Stream.run(handle.stdin),
Effect.ignore,
Effect.forkScoped
);The checked ownership test starts a child that waits indefinitely. When the owner closes, the child must observe interruption before the enclosing Effect returns.¶
Effect.gen(function* childOwnership() {
const started = yield* Deferred.make<boolean>();
const interrupted = yield* Ref.make(0);
yield* Effect.scoped(
Effect.gen(function* ownChild() {
yield* Deferred.succeed(started, true).pipe(
Effect.andThen(Effect.never),
Effect.onInterrupt(() =>
Ref.update(interrupted, (count) => count + 1)
),
Effect.forkScoped
);
yield* Deferred.await(started);
})
);
expect(yield* Ref.get(interrupted)).toBe(1);
})A Deferred is an Effect value completed once to signal that work started. The test does not depend on an arbitrary sleep.¶
Use scopes20 to decide the lifetime before choosing a fork operation. Different fork operations carry different ownership rules; do not treat their names as interchangeable.¶
Consume a stream without promising a bounded queue
The sandbox decodes stdout, splits it into lines, and interprets each protocol message with an Effect. Its consumer stops after the first completed outcome.¶
const outcome = yield* Stream.decodeText(handle.stdout).pipe(
Stream.splitLines,
Stream.filter((line) => line.trim() !== ""),
Stream.mapEffect((line) =>
decodeChildMessage(line).pipe(
Effect.mapError(
(error) =>
new SandboxError({
logs: [],
message: `Unreadable sandbox message: ${error.message}`,
reason: "protocol",
})
),
Stream.filter(Option.isSome),
Stream.map((option) => option.value),
Stream.runHead,
Effect.mapError((error) =>
isSandboxError(error)Stream.mapEffect keeps decoding and dispatch in the stream description. The consumer does not launch eager Promises for each line.¶
Backpressure lets the consumer limit how quickly the producer can continue. An unbounded queue can let the producer keep adding data.¶
The producer uses Queue.unbounded. This code does not prove a bounded producer queue or bounded total memory.
A bounded consumer is not enough when producers can accumulate data faster than it is consumed.¶
For a growing input, choose a stream and inspect each buffer’s policy. For a known finite collection, a bounded traversal can be enough. Measure the service’s limits before selecting an operational bound.¶
Common mistakes
- Create eager Promises before building the owned Effect traversal.¶
- Detach work that should stop with the caller.¶
- Bound worker concurrency and claim every upstream buffer is bounded too.¶
- Copy test counts into production without measuring the provider and host.¶
Effect idiom and house rule
- Fibers, traversal bounds and streams compose owned work without replacing the typed Effect description.¶
- In rat-stack, measure operational limits instead of inventing caps.¶
- In rat-stack, machines own finite domain retries and cancellation20. Effect’s native fibers and schedules still exist.¶
Next, validate HTTP responses7 before treating concurrent provider work as successful. Return to the Effect reading order17.¶
Sources
- Effect contributors. 2026. Effect. Effect 4.0.0.. Effect-TS. forEach limits traversal concurrency and retains input order; forkScoped attaches child interruption to the current Scope. Accessed 2026-10-06.
- Effect contributors. 2026. Stream.. Effect-TS. mapEffect performs effectful transformation; splitLines and runHead consume line-oriented output. Accessed 2026-10-06.
- Effect contributors. 2026. Consuming and transforming streams.. Effect-TS. Effectful stream transformation and consumption with explicit concurrency choices. Accessed 2026-10-06.
- rat-stack contributors. 2026. Subprocess sandbox ownership.. rat-stack. A scoped stdin writer and decoded stdout stream; its producer queue is explicitly unbounded. Accessed 2026-10-06.
- rat-stack contributors. 2026. Traversal and child-ownership fixtures.. rat-stack. Generated traversals check peak active work, completion and order. A separate seam proves scoped child interruption. The bound property catches a planted unbounded traversal. Accessed 2026-10-06.
- Langton, Kit. 2026. Testing. Effect Solutions.. Kit Langton. Fiber examples frame the ownership question; the snapshot has no complete concurrency guide. Independent fixtures establish this page's claims. Accessed 2026-10-06.
Last changed 2026-10-07 in 0e2d656. Source on GitHub. Change log.
Linked from
- Agent guide. An Effect stack so pure (aspirational) Kit Langton will blush.
Concurrent work needs an owner and a bound — Limit active work, retain result order, and stop child work when its owning lifetime ends.
- Change log. What changed in the files served here, newest first.
Concurrent work needs an owner and a bound — edited: feat(learn): teach cards 23 to 42 with plain lines, diagrams and snippets
- Effect basics. An Effect describes work, expected failures, and required services before a runtime executes it.
Concurrency: bound work and stop owned children.¶
- Full agent guide. An Effect stack so pure (aspirational) Kit Langton will blush.
- Rat Stack lore | rat-stack. Short, source-grounded notes on the ideas and decisions behind rat-stack.
Concurrent work needs an owner and a bound — Limit active work, retain result order, and stop child work when its owning lifetime ends.
- Scopes release resources when work ends. Acquire resources lazily, keep their use inside the owner, and release them after success, failure, or interruption.
Then read concurrent work before starting child work.
- Source change history (log.md). The generated change log as Markdown.
Concurrent work needs an owner and a bound — edited: feat(learn): teach cards 23 to 42 with plain lines, diagrams and snippets
Unlinked mentions (5)
- Effect 4 study: September 2026 → Shape: createEffectActor(machine) returns Effect<EffectActor, never, R | Scope> and drives XState 6’s pure transition through xstate/durable on an Effect fiber.
- Effect Days 2025: Building Effect 4 → A fiber is independently running work.
- Hosted agent interfaces → Pending reads must not use a shared promise, fiber, latch, or response body.
- profile-a-worker → In an Effect program, the fiber runtime (runLoop, makePrimitiveProto, succeedWith) is the floor every request pays.
- Scopes release resources when work ends → A fiber is an independently running Effect.
Content updated .