Pipeline graphs often have independent I/O branches, such as retrievers or API-backed components that become ready at the same point. Haystack can schedule those branches concurrently so one slow request does not force another ready branch to wait.
The AsyncPipeline.run_async() method waits for the graph to finish and returns its leaf outputs. A custom component must still provide run() for the component contract; adding run_async() gives the asynchronous pipeline a non-blocking implementation to await. The run_async_generator() method is the alternative when the caller needs component outputs as they arrive.
The concurrency_limit parameter caps the number of components that may run together. The two leaf components in async_pipeline_demo.py share a synchronization gate: each branch waits until the other branch has started, so a sequential execution times out instead of printing a false success result.
Steps to run an async Haystack pipeline:
- Create async_pipeline_demo.py with the required imports and shared branch gate.
- async_pipeline_demo.py
import asyncio from haystack import AsyncPipeline, component class BranchGate: def __init__(self, branch_count: int): self.branch_count = branch_count self.ready = set() self.all_ready = asyncio.Event() async def wait_for_peers(self, branch_name: str): self.ready.add(branch_name) if len(self.ready) == self.branch_count: self.all_ready.set() await asyncio.wait_for(self.all_ready.wait(), timeout=1)
The event opens only after both branch names are present. A pipeline that runs these components one at a time cannot pass the gate.
- Add ConcurrentResponder below BranchGate with synchronous and asynchronous component paths.
@component class ConcurrentResponder: def __init__(self, name: str, gate: BranchGate): self.name = name self.gate = gate @component.output_types(reply=str) def run(self, query: str): raise RuntimeError("AsyncPipeline used the synchronous component path") @component.output_types(reply=str) async def run_async(self, query: str): await self.gate.wait_for_peers(self.name) await asyncio.sleep(0.05) return {"reply": f"{self.name}: {query}"}
run() satisfies the component contract and fails visibly if the synchronous path is selected. run_async() performs the non-blocking work used by AsyncPipeline.
- Add main() below ConcurrentResponder to create two independent pipeline leaves.
async def main(): gate = BranchGate(branch_count=2) pipeline = AsyncPipeline() pipeline.add_component("docs_branch", ConcurrentResponder("docs", gate)) pipeline.add_component("faq_branch", ConcurrentResponder("faq", gate))
Neither leaf depends on the other. Both receive top-level input and become ready together.
- Extend main() with the asynchronous pipeline call and result output.
result = await pipeline.run_async( data={ "docs_branch": {"query": "reset password"}, "faq_branch": {"query": "reset password"}, }, concurrency_limit=2, ) for branch_name in sorted(result): print(result[branch_name]["reply"]) print(f"branches ready together: {', '.join(sorted(gate.ready))}")
A limit of two allows both ready leaves to enter run_async(). A lower limit suits components with stricter rate or resource constraints.
- Append the asyncio entry point after main().
if __name__ == "__main__": asyncio.run(main())
- Run the completed async pipeline script to confirm both branches pass the synchronization gate.
$ python3 async_pipeline_demo.py docs: reset password faq: reset password branches ready together: docs, faq
The two replies prove that both leaf outputs returned. The final line is derived from the shared gate, which cannot open until both asynchronous component calls are active.
Mohd Shakir Zakaria is a cloud architect with deep roots in software development and open-source advocacy. Certified in AWS, Red Hat, VMware, ITIL, and Linux, he specializes in designing and managing robust cloud and on-premises infrastructures.