F#
Async.Sequential
Async.Parallel
concurrency
programming

F Is there a Async.Sequential to match Async.Parallel?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

F# provides Async.Parallel to run multiple async computations concurrently, but there is no built-in Async.Sequential function. The reason is straightforward — sequential execution is just a loop or a chain of let! bindings in an async block. You can implement sequential execution with Async.Sequential as a simple helper that folds over a sequence of async computations, executing each one in order. This article shows how to implement it and when to choose sequential vs parallel execution.

Async.Parallel (Built-in)

fsharp
1open System
2
3let fetchData (url: string) = async {
4    do! Async.Sleep(1000)  // Simulate network delay
5    return sprintf "Data from %s" url
6}
7
8let urls = [| "api/users"; "api/orders"; "api/products" |]
9
10// Run all requests concurrently
11let parallelResults =
12    urls
13| > Array.map fetchData | > Async.Parallel | > Async.RunSynchronously printfn "%A" parallelResults // [ | "Data from api/users"; "Data from api/orders"; "Data from api/products" | ] // Completes in ~1 second (all run concurrently) ``` `Async.Parallel` takes an array of `Async<'T>` and returns `Async<'T[]>`, running all computations concurrently. ## Implementing Async.Sequential ```fsharp module Async = let Sequential (computations: seq<Async<'T>>) : Async<'T[]> = async { let results = ResizeArray<'T>() for computation in computations do let! result = computation results.Add(result) return results.ToArray() } // Usage let sequentialResults = urls | > Array.map fetchData | > Async.Sequential | > Async.RunSynchronously printfn "%A" sequentialResults // Same results, but takes ~3 seconds (one after another) ``` ## Alternative: Using List.mapAsync ```fsharp // Sequential map over a list let mapAsync (f: 'a -> Async<'b>) (items: 'a list) : Async<'b list> = async { let results = ResizeArray<'b>() for item in items do let! result = f item results.Add(result) return results | > Seq.toList } // Usage let results = ["api/users"; "api/orders"; "api/products"] | > mapAsync fetchData | > Async.RunSynchronously ``` ## Sequential with let! Bindings The most explicit way to run computations sequentially is chaining `let!`: ```fsharp let workflow = async { let! users = fetchData "api/users" let! orders = fetchData "api/orders" let! products = fetchData "api/products" return (users, orders, products) } let (u, o, p) = Async.RunSynchronously workflow printfn "Users: %s, Orders: %s, Products: %s" u o p ``` Each `let!` waits for the previous computation to complete before starting the next one. ## Sequential with Error Handling ```fsharp let sequentialWithErrors (computations: seq<Async<'T>>) : Async<Result<'T, exn>[]> = async { let results = ResizeArray<Result<'T, exn>>() for computation in computations do let! result = computation | > Async.Catch | > Async.map (function | Choice1Of2 value -> Ok value | Choice2Of2 exn -> Error exn) results.Add(result) return results.ToArray() } ``` ## Throttled Parallel Execution A middle ground between fully sequential and fully parallel — limit concurrency: ```fsharp let parallelThrottled (maxConcurrency: int) (computations: seq<Async<'T>>) : Async<'T[]> = async { let semaphore = new System.Threading.SemaphoreSlim(maxConcurrency) let throttled computation = async { do! semaphore.WaitAsync() | > Async.AwaitTask try return! computation finally semaphore.Release() | > ignore } return! computations | > Seq.map throttled | > Async.Parallel } // Run at most 3 at a time let results = urls | > Array.map fetchData | > parallelThrottled 3 | > Async.RunSynchronously ``` ## Comparison ```fsharp // Parallel: all at once let parallel = Async.Parallel [ | task1; task2; task3 | ] // Time: max(t1, t2, t3) // Use when: tasks are independent, resources allow concurrency // Sequential: one after another let sequential = Async.Sequential [ | task1; task2; task3 | ] // Time: t1 + t2 + t3 // Use when: tasks depend on each other, or resource limits apply // Throttled: limited concurrency let throttled = parallelThrottled 2 [ | task1; task2; task3 |
14| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
15| `Async.Parallel` | All at once | max(tasks) | Independent tasks, no limits |
16| `Async.Sequential` | One at a time | sum(tasks) | Dependent tasks, ordered results |
17| Throttled parallel | N at a time | Between both | Rate limits, connection pools |
18
19## Common Pitfalls
20
21* **Assuming `Async.Sequential` exists in the standard library**: F# does not include `Async.Sequential` because sequential execution is just a `for` loop with `let!` inside an `async` block. Implement it as a helper function in your codebase.
22* **Using `Async.Parallel` when order matters**: `Async.Parallel` does not guarantee the order results are produced — it returns them in the same order as the input array, but the computations may complete in any order. If one computation depends on the result of another, use sequential execution.
23* **Forgetting that `Async.Parallel` can overwhelm resources**: Running 10,000 HTTP requests in parallel via `Async.Parallel` can exhaust connections, trigger rate limits, or overload the server. Use throttled parallel execution to limit concurrency.
24* **Not handling exceptions in sequential chains**: If one computation in a sequential chain throws, the remaining computations are skipped. Use `Async.Catch` to convert exceptions to `Choice` values if you need to continue processing despite errors.
25* **Blocking with `Async.RunSynchronously` in async contexts**: Calling `RunSynchronously` inside an async workflow can cause deadlocks. Use `let!` to compose async computations and only call `RunSynchronously` at the top-level entry point.
26
27## Summary
28
29* F# has no built-in `Async.Sequential` — implement it as a loop with `let!` inside an `async` block
30* `Async.Parallel` runs all computations concurrently; sequential runs them one after another
31* Chain `let!` bindings for a fixed number of sequential operations
32* Use a custom `Async.Sequential` helper for a dynamic collection of operations
33* Use throttled parallel execution to limit concurrency (e.g., API rate limits)
34* Only call `Async.RunSynchronously` at the top-level entry point — use `let!` for composition

Course illustration
Course illustration

All Rights Reserved.