Skip to main content

Command Palette

Search for a command to run...

Building a Worker Pool in F# with MailboxProcessor

Updated
3 min read
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 number

  • Stop → 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.