Skip to content

Batching

Real programs spend a lot of time talking to slow, remote things: databases, HTTP APIs, caches. When a single logical operation fans out into dozens of individual lookups — one query per user, one fetch per id — the naive approach issues one round-trip per call. The network round-trips, not the CPU, become the bottleneck.

Effect’s batching system lets you describe what you need (a Request) separately from how it is fetched (a RequestResolver). When many requests are made — even across concurrent fibers — Effect collects them into a single batch, hands them to the resolver together, and automatically deduplicates identical requests. Your business logic keeps asking for one thing at a time; the runtime turns that into one efficient external call.

import { Effect } from "effect"
// `getUserById` looks like a simple per-id lookup...
declare const getUserById: (id: number) => Effect.Effect<string>
// ...but running many of them concurrently produces a *single* batched
// call to the underlying resolver, with duplicate ids (1 and 2) collapsed.
const program = Effect.forEach([1, 2, 1, 3, 2], getUserById, {
concurrency: "unbounded"
})
  • A Request is a typed description of one piece of data you want — its inputs, its success type, and its error type. Requests are plain values, so two identical requests are equal and can be deduplicated.
  • A RequestResolver receives a non-empty batch of pending requests and completes each one. This is the only place that touches the external system, so it is where you write a bulk query (WHERE id IN (...)) or a batched API call.
  • Effect.request(req, resolver) ties them together. The runtime gathers all requests made within a short delay window (across concurrent fibers), passes the unique set to the resolver, and routes each result back to the fiber that asked for it.
  • Request & RequestResolver — define request types, write a batching resolver, deduplicate, add a delay window, tracing, and a cache.
  • Caching memoizes effects by input; batching collapses concurrent calls into one round-trip. The two compose — RequestResolver.withCache adds caching on top of a resolver.
  • Concurrency — batching shines when requests are issued from many fibers with Effect.forEach(..., { concurrency }).
  • Observability — resolvers can wrap each batch in a span so you can see exactly how many requests collapsed into one call.