Durable Fan-Out Fan-In¶
Overview¶
This recipe documents the parallel orchestration pattern where one orchestrator schedules
multiple activities and waits for all of them with context.task_all().
The example starts from an HTTP endpoint, creates five work items, and processes them
simultaneously.
Durable replay still applies. The orchestrator does not execute work itself; it only schedules activity tasks and deterministically awaits completion. This gives parallel throughput while preserving fault-tolerant history semantics.
When to Use¶
- You have independent units of work that can run safely in parallel.
- You want to reduce total workflow latency compared with sequential activity chaining.
- You need checkpointed orchestration so partial completion survives host restarts.
Architecture¶
+--------+ POST /api/start-fanout +---------------------------+
| Client | --------------------------------> | HTTP starter |
+---+----+ +------------+--------------+
| |
| 202 + status URLs | start_new()
v v
+------------------------+ +----------------------------+
| Orchestration Instance | | fan_out_fan_in_orchestrator|
+-----------+------------+ | items = item-1..item-5 |
| | tasks = call_activity(*) |
| schedule 5 activities | yield context.task_all(...)|
v +-------------+--------------+
+--------+---------+--------+--------+ |
| process item-1 ... process item-5 v
+--------+---------+--------+--------+ +------------------------+
| results[5] returned |
+------------------------+
Prerequisites¶
- Python 3.10+
- Azure Functions Core Tools v4
- Durable storage configured in local settings
azure-functionsandazure-functions-durabledependencies installed
Project Structure¶
examples/durable/durable_fan_out_fan_in/
|- function_app.py
|- host.json
|- local.settings.json.example
|- requirements.txt
`- README.md
Implementation¶
The app uses the same durable blueprint registration pattern used across durable examples.
The starter endpoint creates the instance by orchestrator name.
@bp.route(route="start-fanout", methods=["POST"], auth_level=func.AuthLevel.ANONYMOUS)
@bp.durable_client_input(client_name="client")
async def start_fanout(req: func.HttpRequest, client: df.DurableOrchestrationClient) -> func.HttpResponse:
instance_id = await client.start_new("fan_out_fan_in_orchestrator")
return client.create_check_status_response(req, instance_id)
The orchestrator creates five item IDs, schedules one activity per item,
then blocks on task_all to gather all outputs.
@bp.orchestration_trigger(context_name="context")
def fan_out_fan_in_orchestrator(context: df.DurableOrchestrationContext):
items = [f"item-{index}" for index in range(1, 6)]
tasks = [context.call_activity("process_item", item) for item in items]
results = yield context.task_all(tasks)
return results
The activity is intentionally small so orchestration behavior is clear.
@bp.activity_trigger(input_name="payload")
def process_item(payload: str) -> str:
return f"Processed {payload}"
Replay model note:
task_all does not violate determinism because the task list is built from fixed input.
Do not inject random values or direct network I/O in the orchestrator body.
Run Locally¶
Expected Output¶
POST /api/start-fanout -> 202 Accepted
Final orchestration output:
[
"Processed item-1",
"Processed item-2",
"Processed item-3",
"Processed item-4",
"Processed item-5"
]
Production Considerations¶
- Scaling: cap batch size to protect downstream systems from excessive parallel fan-out.
- Retries: wrap each activity with retry options for transient failures.
- Idempotency: each
process_itemexecution should tolerate retries and duplicate delivery. - Observability: emit per-item correlation IDs and aggregate completion metrics.
- Security: avoid exposing anonymous starter endpoints in shared or public environments.