Skip to content

F# Guide — MailboxProcessor: Agent-Based Concurrency

DodaTech Updated 2026-06-28 5 min read

In this tutorial, you will learn about F# Guide. We cover key concepts, practical examples, and best practices to help you master this topic.

MailboxProcessor (also called agents) in F# implements the Actor Model where each agent has isolated state, communicates through message passing, and processes one message at a time for safe concurrency.

What You'll Learn

  • Creating a MailboxProcessor
  • Posting and receiving messages
  • Discriminated unions as message types
  • Shared state management
  • Error handling in agents

Why It Matters

Agents eliminate shared-memory concurrency bugs by encapsulating state and processing messages sequentially. Durga Antivirus Pro uses agents for coordinating concurrent scan workers.

Real-World Use

Agents manage shared state in concurrent systems: chat servers, task queues, event processing, and state machines.

flowchart LR
    A["MailboxProcessor"] --> B["Creation"]
    B --> C["Messages"]
    C --> D["State Management"]
    D --> E["Patterns"]
    A:::current --> B
    style A fill:#2563eb,stroke:#2563eb,color:#fff
    style B fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style C fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style D fill:#dbeafe,stroke:#2563eb,color:#1e40af
    style E fill:#f1f5f9,stroke:#94a3b8,color:#64748b

Basic MailboxProcessor

// Agent that echoes messages
let echoAgent = MailboxProcessor.Start(fun inbox ->
    let rec loop () = async {
        let! msg = inbox.Receive()
        printfn "Received: %s" msg
        return! loop ()
    }
    loop ())

// Send a message
echoAgent.Post "Hello agent!"

Message Types with DU

type CounterMessage =
    | Increment of int
    | Decrement of int
    | GetCount of AsyncReplyChannel<int>
    | Reset

let counterAgent = MailboxProcessor.Start(fun inbox ->
    let rec loop count = async {
        let! msg = inbox.Receive()
        match msg with
        | Increment n -> return! loop (count + n)
        | Decrement n -> return! loop (count - n)
        | GetCount reply ->
            reply.Reply(count)
            return! loop count
        | Reset -> return! loop 0
    }
    loop 0)

// Usage
counterAgent.Post(Increment 5)
counterAgent.Post(Increment 3)
counterAgent.Post(Decrement 2)

let count = counterAgent.PostAndReply(fun reply -> GetCount reply)
// count = 6

Stateful Agent

type Task = { Id: int; Description: string; Completed: bool }

type TaskMessage =
    | Add of Task
    | Complete of int
    | List of AsyncReplyChannel<Task list>
    | Clear

let taskAgent = MailboxProcessor.Start(fun inbox ->
    let rec loop tasks = async {
        let! msg = inbox.Receive()
        match msg with
        | Add task -> return! loop (task :: tasks)
        | Complete id ->
            let updated = tasks |> List.map (fun t ->
                if t.Id = id then { t with Completed = true } else t)
            return! loop updated
        | List reply ->
            reply.Reply(tasks)
            return! loop tasks
        | Clear -> return! loop []
    }
    loop [])

taskAgent.Post(Add { Id = 1; Description = "Learn F#"; Completed = false })
taskAgent.Post(Add { Id = 2; Description = "Build app"; Completed = false })
taskAgent.Post(Complete 1)

let tasks = taskAgent.PostAndReply(List)

Error Handling

type SafeMessage =
    | Process of string
    | Error of exn
    | GetStatus of AsyncReplyChannel<string>

let safeAgent = MailboxProcessor.Start(fun inbox ->
    let rec loop status = async {
        try
            let! msg = inbox.Receive()
            match msg with
            | Process data ->
                // Potentially failing operation
                let result = riskyOperation data
                return! loop (sprintf "OK: %s" result)
            | Error ex ->
                return! loop (sprintf "Error: %s" ex.Message)
            | GetStatus reply ->
                reply.Reply(status)
                return! loop status
        with ex ->
            return! loop (sprintf "Exception: %s" ex.Message)
    }
    loop "Ready")

Scanning for Messages

type Message =
    | Priority of string
    | Normal of string

let priorityAgent = MailboxProcessor.Start(fun inbox ->
    let rec loop () = async {
        let! msg = inbox.Scan(fun msg ->
            match msg with
            | Priority _ -> Some (async {
                match msg with
                | Priority data -> printfn "Priority: %s" data
                | _ -> ()
                return! loop ()
              })
            | Normal _ -> None  // Don't process yet
        )
        ()
    }
    loop ())

// Alternative: check all messages
let! msg = inbox.Receive()  // Normal order
let! msg = inbox.TryReceive(timeout=1000)  // With timeout

Agent with Cancellation

let cancellableAgent ct = MailboxProcessor.Start((fun inbox ->
    let rec loop () = async {
        if ct.IsCancellationRequested then
            printfn "Agent cancelled"
            return ()
        else
            let! msg = inbox.Receive()
            printfn "Processing: %s" msg
            return! loop ()
    }
    loop ()), ct)

Common Mistakes

1. Blocking inside agent

Agent processes messages on one thread. Blocking operations stall the entire agent.

2. Forgetting reply channels

Use AsyncReplyChannel<'T> in messages to get responses from agents.

3. Unbounded Message Queues

Agents buffer messages in memory. Use bounded agents for backpressure.

4. Shared mutable state

Don't pass mutable state between agents. Copy data in messages.

5. Exception Handling

Unhandled exceptions crash the agent. Always catch exceptions in the message loop.

Practice Questions

1. What is a MailboxProcessor? An F# agent that processes messages sequentially in its own isolated state, implementing the actor model.

2. How do you get a reply from an agent? Include an AsyncReplyChannel<'T> in your message type and use PostAndReply.

3. What is the difference between Receive and Scan? Receive gets the next message. Scan searches for a specific message type, potentially skipping others.

Challenge: Build a key-value store agent that supports get, set, and delete operations.

FAQ

{{< faq question="Are MailboxProcessors like Erlang actors?" >}} Yes, the model is similar. Each agent has isolated state, communicates via messages, and processes one message at a time. {{< /faq >}}

{{< faq question="How many agents can I create?" >} Thousands. Each agent uses minimal overhead. They are lightweight like Erlang processes. {{< /faq >}}

{{< faq question="What happens if an agent crashes?" >} An unhandled exception terminates the agent. Wrap the message loop in try-with to handle errors gracefully. {{< /faq >}}

{{< faq question="Can agents share state?" >} No. Each agent has its own isolated state. If they need to coordinate, they send messages to each other. {{< /faq >}}

{{< faq question="How do I stop an agent?" >} Send a Stop message or use IDisposable/CancellationToken. Agents can also simply let the loop function return. {{< /faq >}}

Mini Project

Build a logging agent with multiple log levels:

type LogLevel = Debug | Info | Warning | Error
type LogMessage = {
    Level: LogLevel
    Timestamp: System.DateTime
    Message: string
}

type LoggerMessage =
    | Log of LogMessage
    | GetLogs of AsyncReplyChannel<LogMessage list>
    | ClearLogs

let loggerAgent = MailboxProcessor.Start(fun inbox ->
    let rec loop logs = async {
        let! msg = inbox.Receive()
        match msg with
        | Log log -> return! loop (log :: logs)
        | GetLogs reply ->
            reply.Reply(List.rev logs)
            return! loop logs
        | ClearLogs -> return! loop []
    }
    loop [])

let log level message =
    { Level = level; Timestamp = System.DateTime.UtcNow; Message = message }
    |> Log
    |> loggerAgent.Post

What's Next

Now that you understand agents, explore query expressions for data source queries.

Topic Description Link
F# Queries Query expressions {{< ref "21-queries" >}}
F# Type Providers Data type providers {{< ref "22-type-providers" >}}

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro