| name | effect-stream |
| description | Effect Stream for lazy, unbounded, or resource-scoped sequences. Use when data is paginated, infinite, or needs backpressure. |
| user-invocable | false |
Effect Stream
Stream<A, E, R> is a pull-based, lazy description of a program that emits zero or more values. Where Effect<A, E, R> always produces exactly one value, Stream handles finite, infinite, or empty sequences with inherent laziness and backpressure.
For finite, materialized collections, see /effect-iteration.
When to Use Stream
| Use Stream when... | Use Effect.forEach when... |
|---|
| Data is fetched lazily (pagination, cursor) | Collection is already in memory |
| Sequence is potentially infinite | Size is known and bounded |
| Processing needs backpressure | All results needed before continuing |
| Resources must be scoped to iteration | No resource lifecycle concerns |
| Multi-stage transformation pipeline | Simple one-pass processing |
Creating Streams
From Existing Data
const stream = Stream.fromIterable([1, 2, 3]);
const stream = Stream.fromEffect(fetchUser(id));
const stream = Stream.fromChunk(chunk);
Lazy Generation
const pages = Stream.unfoldEffect(firstPageToken, (token) =>
fetchPage(token).pipe(
Effect.map((page) =>
page.nextToken ? Option.some([page.data, page.nextToken] as const) : Option.none(),
),
),
);
const naturals = Stream.iterate(1, (n) => n + 1);
const firstFive = naturals.pipe(Stream.take(5));
const heartbeats = Stream.repeatEffect(ping());
Stream Operations
Transformations
const pipeline = Stream.fromIterable(rawEvents).pipe(
Stream.filter((e) => e.type === "purchase"),
Stream.map((e) => ({ userId: e.userId, amount: e.amount })),
Stream.mapEffect((e) => enrichWithUserData(e)),
);
Concurrent Processing
Stream.mapEffect accepts concurrency options:
Stream.fromIterable(urls).pipe(Stream.mapEffect((url) => fetchUrl(url), { concurrency: 5 }));
Stream.mapEffect((url) => fetchUrl(url), { concurrency: 5, unordered: true });
Batching
const batched = stream.pipe(Stream.grouped(100));
const microBatched = stream.pipe(
Stream.groupedWithin(100, "5 seconds"),
Stream.mapEffect((batch) => insertBatch(batch)),
);
Early Termination
const until100 = Stream.iterate(0, (n) => n + 1).pipe(
Stream.takeWhile((n) => n < 100),
);
const untilDone = stream.pipe(Stream.takeUntil((msg) => msg.type === "done"));
Merging Streams
const merged = Stream.merge(stream1, stream2);
const merged = Stream.merge(stream1, stream2, { haltStrategy: "either" });
const all = Stream.mergeAll([s1, s2, s3], { concurrency: 3 });
Backpressure and Buffering
Pull-based model provides inherent backpressure: slow consumer throttles producer.
For decoupling producer/consumer speeds:
const buffered = Stream.range(1, 10000).pipe(
Stream.mapEffect(processItem),
Stream.buffer({ capacity: 64 }),
);
Resource-Scoped Iteration
Stream.acquireRelease ties element production to resource lifecycles:
const rows = Stream.acquireRelease(openDatabaseConnection(), (conn) => conn.close()).pipe(
Stream.flatMap((conn) => Stream.fromIterable(conn.queryIterator("SELECT * FROM users"))),
);
Other resource patterns:
const scoped = Stream.scoped(acquireScopedResource());
const withCleanup = stream.pipe(Stream.ensuring(Effect.log("Stream finished")));
Consuming Streams
| Method | Use When |
|---|
Stream.runCollect(stream) | Need full result set (bounded only!) |
Stream.runForEach(stream, fn) | Side-effect processing, no collection |
Stream.runFold(stream, init, fn) | Aggregation without materialization |
Stream.runDrain(stream) | Run for effects only, discard values |
Stream.run(stream, sink) | Custom consumption via Sink |
Stream.runCollect materializes the entire stream into a Chunk. Use only for bounded streams. For side-effect processing, prefer Stream.runForEach.
const items = yield * Stream.runCollect(stream);
const array = Chunk.toReadonlyArray(items);
yield * Stream.runForEach(stream, (item) => processItem(item));
const sum = yield * Stream.runFold(stream, 0, (acc, n) => acc + n);
Chunk: Stream's Native Collection
Chunk<A> is optimized for repeated concatenation—the pattern inside Stream pipelines. You'll encounter it as the return type of Stream.runCollect.
const chunk = yield * Stream.runCollect(stream);
const array = Chunk.toReadonlyArray(chunk);
For general collection guidance, see /effect-collections.
Anti-Patterns to Avoid
Over-abstracting with Stream for small arrays. If you have 10 items in memory, Effect.forEach(items, callApi, { concurrency: 5 }) is simpler than Stream.fromIterable(items).pipe(Stream.mapEffect(callApi), Stream.runCollect).
Collecting entire Stream unnecessarily. Calling Stream.runCollect on 10 million rows defeats the purpose. Use Stream.runForEach or Stream.runFold.
Forgetting to consume. A Stream is a description—it does nothing until you run it.
Decision Matrix
| Situation | Pattern | Key API |
|---|
| Paginated API / database cursor | Stream.unfoldEffect | Lazy page fetching |
| Infinite or unbounded sequence | Stream.iterate/repeatEffect | Pull-based, safe to define |
| Multi-stage transform pipeline | Stream operators | map → filter → mapEffect |
| Resource-scoped iteration | Stream.acquireRelease | Guaranteed cleanup |
| Concurrent processing with backpressure | Stream.mapEffect | { concurrency: N } |
| Time-windowed batching | Stream.groupedWithin | Count OR time trigger |
| Merging event sources | Stream.merge/mergeAll | Concurrent interleaving |
| Known finite collection | Effect.forEach | See /effect-iteration |
Effect Stream Checklist