Fan-Out: Dispatching One Input to Many Parallel Consumers
Concept
Fan-out is the pattern where a single trigger — one event, one input, one orchestrator step — dispatches work to multiple independent downstream consumers that execute concurrently rather than sequentially. It shows up at every layer of a system: an event bus/pub-sub topic fanning one message out to N subscribers, an orchestration engine (Azure Durable Functions, AWS Step Functions) fanning out to N parallel activity invocations, or plain application code dispatching N concurrent goroutines/coroutines/promises. The dispatching step doesn't wait for or coordinate what each consumer does with its own copy of the work — that's what distinguishes fan-out from a saga's sequential steps or a pipeline's staged handoff.
Fan-out is usually the first half of a two-part pattern; collecting the parallel results back into one place afterward is fan-in (see Fan-In). A workflow that does both is often called "fan-out/fan-in" as one named pattern — Azure's Durable Functions documentation uses this exact term — even though the two halves solve genuinely separate problems: fan-out is about dispatch and concurrency, fan-in is about aggregation and partial-failure handling.
Mechanically, fan-out needs three decisions that determine its failure characteristics: (1) is the task list known upfront, or built dynamically (e.g. "for each device in this rollout," where the count isn't known until query time); (2) is concurrency bounded (a fixed worker pool / semaphore) or unbounded (fire every task at once); (3) does one consumer's failure affect the others at all, or is each consumer's outcome tracked independently. An orchestration engine handles the dispatch-and-wait mechanics for you, but these three decisions are still the caller's to make.
Tradeoffs
| Approach | Benefit | Cost |
|---|---|---|
| Unbounded fan-out (fire all N at once) | Simplest to write; lowest total latency when all N succeed | N concurrent calls can overwhelm a downstream with no backpressure; resource exhaustion (connections, threads) can starve the caller even though the code looks correct |
| Bounded fan-out (worker pool / semaphore of size K) | Protects both the caller's resources and the downstream from a burst of N simultaneous calls | Total latency grows once N > K, since the (K+1)th task waits for a slot to free |
| Sequential dispatch (no fan-out at all) | Zero concurrency to reason about; simplest possible failure model | Total latency is the sum of all N calls instead of the max — unacceptable once N is more than a handful |
The real decision isn't "fan-out or not" once there's more than a couple of downstream calls — it's how much concurrency to allow, which is a capacity question about the downstream, not the caller.
When to use / when not to
- Use whenever one input needs to reach multiple independent downstream systems, or perform the same operation across many independent units of work (many devices, many partitions, many API calls) that don't need to coordinate with each other.
- Especially relevant at fleet scale — dispatching a deployment or health check to thousands of independent devices is a fan-out where the concurrency-bound decision above becomes the dominant engineering concern (see RateLimiting for why "in parallel" still needs a cap).
- Don't reach for fan-out when the downstream calls have a genuine ordering dependency — that's a pipeline or a saga, not a fan-out.
- Skip it for a single downstream call — fan-out's entire value is coordinating multiple consumers; wrapping one call in a parallel-dispatch framework adds ceremony for no benefit.
Common pitfall
Treating a dynamically-sized task list as if it were small and fixed, then discovering the orchestration framework's checkpointing/history mechanism doesn't scale to very large fan-outs. Azure Durable Functions' own guidance flags this directly: fan-out works best when task counts are known and reasonably bounded, because monitoring hundreds of thousands of simultaneous parallel branches adds real overhead to the orchestrator's checkpointing, and a single activity failing partway through a long-running fan-out can leave the whole orchestration in a partially-completed state that has to be restarted. A rollout dispatched to an entire large fleet as one giant unbounded fan-out, rather than in bounded batches, can make the orchestrator itself the bottleneck — the fix is usually a bounded worker-pool fan-out (batches of K) rather than one unbounded fan-out of everything at once.
Engineering Lens
The question that actually matters in a fan-out design isn't "how do I dispatch N things in parallel" — every language and orchestration framework answers that trivially. It's "what's my concurrency ceiling, and what happens to the caller and the downstream when N exceeds it." A design that can name that ceiling and justify it against the downstream's actual capacity has done the real work; one that just wraps everything in Promise.all/Task.WhenAll and calls it done has pushed an unbounded-concurrency risk into production, waiting for the day N is large enough to matter.
Sources
- Fan-out/fan-in scenarios in Durable Functions — Microsoft Learn / Azure Docs
- Parallelization and scatter-gather patterns — AWS Prescriptive Guidance