Building a Worker Pool in F# with MailboxProcessor

Introduction
While studying concurrent network programming, I explored how to implement a worker pool in F# using MailboxProcessor, F#'s built-in actor abstraction.
The example launches multiple workers that process port numbers concurrently. A CountdownEvent is used to wait until all work items have been completed before shutting down the workers gracefully.
This approach is conceptually similar to Go's goroutines and channels but follows a more actor-oriented design.
Core Components
The implementation consists of three main pieces:
| Component | Responsibility |
|---|---|
| MailboxProcessor | Worker actor |
| Message Type | Communication protocol |
| CountdownEvent | Completion tracking |
Defining Messages
Workers communicate through strongly typed messages.
type Message =
| Port of int
| Stop
This provides a clear protocol:
Port→ process a port numberStop→ terminate the worker
Unlike string-based messaging, the compiler verifies message correctness.
Creating a Worker
Each worker runs as an independent actor.
let worker (id: int) =
MailboxProcessor.Start(fun inbox ->
let rec loop () = async {
let! msg = inbox.Receive()
match msg with
| Port p ->
printfn "Worker %d processed port %d" id p
return! loop ()
| Stop ->
()
}
loop ()
)
Important characteristics:
Sequential message processing
No explicit locking
Independent execution context
Message-driven behavior
Tracking Completion
To emulate Go's WaitGroup, the example uses CountdownEvent.
let completed = CountdownEvent(1024)
Each processed port decrements the counter.
completed.Signal() |> ignore
The main thread blocks until all tasks are finished.
completed.Wait()
Creating the Worker Pool
A pool of 100 workers is created.
let workers =
[|
for i in 1 .. 100 ->
worker i
|]
Each worker owns its mailbox and processes incoming messages independently.
Dispatching Work
Port numbers are distributed across workers.
for port in 1 .. 1024 do
let index = port % 100
workers.[index].Post(Port port)
This creates a simple round-robin scheduling strategy.
Graceful Shutdown
After all work has completed, a termination message is sent.
for w in workers do
w.Post(Stop)
This avoids abruptly terminating active workers.
Go vs F# Concurrency
The design maps naturally to concepts familiar to Go developers.
| Go | F# |
|---|---|
| Goroutine | MailboxProcessor |
| Channel | Mailbox |
| WaitGroup | CountdownEvent |
| Send Message | Post |
| Close Channel | Stop Message |
Although the implementation style differs, both approaches rely on message passing rather than shared-state synchronization.
Why MailboxProcessor?
MailboxProcessor offers several advantages:
Actor-style concurrency
Strongly typed messages
No manual lock management
Clear separation of worker responsibilities
Scales well for network and backend services
This makes it a useful building block for:
Port scanners
TCP servers
Telemetry systems
Chat applications
Distributed services
IoT backends
Conclusion
F#'s MailboxProcessor provides a concise and powerful way to implement concurrent worker pools. By combining actors with typed messages and synchronization primitives such as CountdownEvent, I can build systems that are both scalable and easier to reason about than traditional shared-memory approaches.



