Dispatchers
This guide explains how to use dispatchers to control thread execution for your actors.
What are Dispatchers?
A dispatcher is the "engine" that makes Pekko Actors work. It is responsible for selecting which actors receive messages and allocating threads from the thread pool.
By default, all actors use Pekko's default dispatcher, but you can configure actors to use different dispatchers based on their workload characteristics.
Why Use Different Dispatchers?
The main reason to use different dispatchers is to isolate blocking operations from the default dispatcher. If you perform blocking operations (like blocking I/O) on the default dispatcher, it can starve the thread pool and prevent other actors from processing messages.
Dispatcher Selection API
Spring Boot Starter Actor provides a fluent API for selecting dispatchers when spawning actors:
Default Dispatcher
Use the default Pekko dispatcher (this is the default behavior):
SpringActorRef<MyActor.Command> actor = actorSystem
.actor(MyActor.class)
.withId("my-actor")
.withDefaultDispatcher() // Optional - this is the default
.spawnAndWait();
Blocking Dispatcher
Use Pekko's default blocking I/O dispatcher for actors that perform blocking operations:
SpringActorRef<DatabaseActor.Command> dbActor = actorSystem
.actor(DatabaseActor.class)
.withId("db-actor")
.withBlockingDispatcher() // Use blocking I/O dispatcher
.spawnAndWait();
When to use:
- Database operations
- File I/O
- Network calls (blocking APIs)
- Any operation that blocks the thread
Custom Dispatcher from Configuration
Use a custom dispatcher defined in your application configuration:
SpringActorRef<WorkerActor.Command> worker = actorSystem
.actor(WorkerActor.class)
.withId("worker")
.withDispatcherFromConfig("my-custom-dispatcher")
.spawnAndWait();
Same-as-Parent Dispatcher
Inherit the dispatcher from the parent actor (useful for child actors):
SpringActorRef<ChildActor.Command> child = parentRef
.child(ChildActor.class)
.withId("child")
.withDispatcherSameAsParent()
.spawnAndWait();
Types of Dispatchers
Dispatcher (Default)
The default dispatcher is used when no specific dispatcher is configured. It uses a fork-join-executor by default and provides excellent performance for non-blocking operations.
Fork-Join Executor
The fork-join executor is a work-stealing thread pool that provides efficient CPU utilization for non-blocking workloads.
PinnedDispatcher
A PinnedDispatcher dedicates a unique thread for each actor using it. This can be useful for actors that need thread-local state or for bulkheading critical actors.
Resource Usage
PinnedDispatcher creates one thread per actor, which can lead to high resource consumption. Use sparingly and only when necessary.
Configuring Custom Dispatchers
Define custom dispatchers in your application.yml:
Fork-Join Executor
The fork-join-executor is a work-stealing thread pool:
spring:
actor:
my-dispatcher:
type: Dispatcher
executor: fork-join-executor
fork-join-executor:
# Min number of threads
parallelism-min: 2
# Thread count = ceil(available processors * factor)
parallelism-factor: 2.0
# Max number of threads
parallelism-max: 10
# Throughput defines the maximum number of messages
# to be processed per actor before the thread jumps
# to the next actor. Set to 1 for as fair as possible.
throughput: 100
Thread Pool Executor
The thread-pool-executor is based on a java.util.concurrent.ThreadPoolExecutor:
spring:
actor:
my-blocking-dispatcher:
type: Dispatcher
executor: thread-pool-executor
thread-pool-executor:
# Fixed pool size
fixed-pool-size: 16
throughput: 1
Dynamic Thread Pool Executor
For variable pool sizes:
spring:
actor:
my-dynamic-dispatcher:
type: Dispatcher
executor: thread-pool-executor
thread-pool-executor:
core-pool-size-min: 4
core-pool-size-factor: 2.0
core-pool-size-max: 16
max-pool-size-min: 8
max-pool-size-factor: 2.0
max-pool-size-max: 32
throughput: 10
Virtual Thread Executor (Java 21+)
If you're running on Java 21 or later, you can use Pekko's built-in virtual thread executor for actors performing blocking I/O operations. Virtual threads are lightweight threads that provide excellent scalability for blocking operations.
Benefits of Virtual Threads:
- Very low memory overhead (~1KB per thread vs ~1MB for platform threads)
- Can handle millions of concurrent operations
- Ideal for blocking I/O (database calls, HTTP requests, file operations)
- No need for reactive programming patterns
Java 21+ Required
Virtual thread support requires Java 21 or later. If you're on Java 11-17, use thread-pool-executor instead.
Configuration:
Usage:
SpringActorRef<DatabaseActor.Command> dbActor = actorSystem
.actor(DatabaseActor.class)
.withId("db-actor")
.withDispatcherFromConfig("virtual-thread-dispatcher")
.spawnAndWait();
JVM Tuning (Optional):
You can further configure virtual threads using JVM system properties:
jdk.virtualThreadScheduler.parallelism- Number of platform threads for virtual thread schedulingjdk.virtualThreadScheduler.maxPoolSize- Maximum pool size for virtual thread schedulerjdk.unparker.maxPoolSize- Maximum pool size for unparking virtual threads
When to Tune
Most applications don't need to tune these settings. Only adjust if you've identified specific bottlenecks through profiling.
Requirements: - Java 21 or later - Suitable for blocking I/O operations only (not CPU-intensive tasks)
When to use Virtual Threads vs Thread Pool:
| Use Case | Recommended Dispatcher |
|---|---|
| Blocking I/O (Java 21+) | Virtual Thread Executor |
| Blocking I/O (Java 11-17) | Thread Pool Executor |
| CPU-intensive tasks | Fork-Join Executor (default) |
| Non-blocking operations | Default Dispatcher |
Virtual Threads Example
See the virtual-threads example for a complete working demonstration of virtual threads with actors.
Blocking Operations
The most important reason to use a separate dispatcher is to isolate blocking operations from the default dispatcher.
Problem:
If you have blocking operations (such as blocking I/O, database calls, or expensive computations) and run them on the default dispatcher, it will block threads that are needed for other actors to process their messages. This can cause your application to become unresponsive.
Solution:
Always use a separate dispatcher with a thread pool executor for actors that perform blocking operations. Use .withBlockingDispatcher() or a custom dispatcher with a thread-pool-executor.
Thread Starvation
Running blocking operations on the default dispatcher can lead to thread starvation, where all threads are blocked waiting for I/O, preventing other actors from processing messages.
Example configuration for blocking operations:
spring:
actor:
my-blocking-dispatcher:
type: Dispatcher
executor: thread-pool-executor
thread-pool-executor:
fixed-pool-size: 16
throughput: 1
Throughput Configuration
The throughput setting defines the maximum number of messages to be processed per actor before the thread jumps to the next actor. Set to 1 for as fair as possible.
Higher throughput values can improve performance by reducing the number of context switches, but may increase latency for individual messages.
More Information
For more detailed information about dispatchers, refer to the Pekko Dispatcher Documentation.
Next Steps
- Actor Registration - Learn how to create and spawn actors
- Routers - Use routers for load balancing and parallel processing
- Logging with MDC - Enhance observability with MDC and tags