Robyn H
07/23/2025, 7:29 AMasyncio.gather(*[sub_flow(x) for x in params_set]) - type statement, and testing with a local self-hosted Prefect server. It's important for our use-case that developers can run the workflow themselves locally with a self-hosted server.
What we observed for 10k subflows were http PoolTimeout errors. Following some tips on the forum, we increased the PREFECT_API_REQUEST_TIMEOUT and set PREFECT_API_ENABLE_HTTP2 to False. Now, we no longer see the PoolTimeout error, but it is a major issue how long it takes the server to register the sub-flows - first as task runs, then as subflow runs, and then to actually start running them.
E.g. for 2.5k locations, it took 6 minutes to start actually executing the subflows, but for 5k it took 30 minutes, i.e. it doesn't increase linearly. As stated previously, we would like to support spawning at least hundreds of thousands of subflows. As this seems to be a limitation on the Prefect server side in terms of the number of incoming requests, I don't think there is much we can change in our code.
What is the explanation for this? And is there any workaround we can perform without having to scale up the Prefect server with multiple API servers? It would be an issue for us in terms of developer experience if each developer has to set up a scaled server rather than just quick-starting Prefect locally.
Would appreciate any advice, thank you!Kevin Grismore
07/23/2025, 3:17 PMprefect>=3.0.0, task runs update their state and have their orchestration rules enforced directly in the client, and emit events which the server receives and uses to update reporting on the state of task runs. This means there's a much smaller chance a high volume of task runs is going to lead to network issues, since events are emitted over websockets.Kevin Grismore
07/23/2025, 3:19 PMmy_task.map(params_set), which will start a new python thread running an instance of your task for each item in your iterable param. In the older days of prefect we had some rules about nesting, but those don't exist anymore. Flows can call tasks, tasks can call tasks, tasks can call flows - it doesn't matter. For the API and network reasons described above, that means tasks are pretty much always the better choice for high workload volume.Robyn H
07/24/2025, 12:01 PMKevin Grismore
07/24/2025, 12:49 PM.map() then the returned object is a list[PrefectFuture]. A PrefectFuture is an extension of a regular Python Future in that it's a placeholder object for work that has been dispatched elsewhere (in this case a different thread) so that your code can proceed. You can call methods on it at any time later on to check on the state of that dispatched work (future.state()), wait for that work to finish as a blocking call (future.wait()), or wait for that work to finish as a blocking call and retrieve its return value (future.result()). We offer a utility function for waiting on the completion of a list of futures you can use, from prefect.futures import wait that can be used something like this:
futures = my_task.map(param_set)
wait(futures)
That'll keep your flow run running until all your tasks are complete.
2. Events are processed as a queue, so they're eventually consistent, but how long that queue takes to get through depends on a number of factors. I'm personally not that familiar with how we handle the events system in a minimal OSS setup since I spend most of my time on Prefect Cloud, but @Nate might be able to offer some insight here. One more thing to consider is what your tasks are really doing. Is it possible that they're doing some sync or CPU-bound work that's limiting the benefits of Python concurrency? Either way, a scale of hundreds of thousands of concurrent tasks may need some additional considerations on your server setup or how you choose to limit the concurrency of your work.Nate
07/24/2025, 5:48 PMRobyn H
07/29/2025, 6:47 AMNate
07/30/2025, 8:27 PM