Parallel Execution in Ballerina: Using Workers for CPU-Intensive Tasks
Learn how to use Ballerina workers to offload CPU-intensive tasks from the main event loop, utilizing futures for asynchronous results and locks for thread safety.
16 Oct 2026, 21:37 UTC

Preventing Event Loop Blockage with Workers
In Ballerina, the main execution flow relies on an asynchronous event loop to handle I/O operations. While this is efficient for network calls, executing a CPU-intensive calculation (like heavy data transformation or complex cryptography) on the main thread will freeze the event loop, preventing the application from responding to other requests. To solve this, Ballerina provides the worker keyword, which offloads execution to a separate OS thread.
The Worker Mechanism
A worker is a structured concurrency primitive that allows a block of code to run in parallel with the main thread. Because workers run in separate threads, they do not share the same execution context. To retrieve a result from a worker without blocking the main thread immediately, Ballerina uses the future type—a placeholder for a value that will be available at some point in the future.
Implementation Example: Parallel Calculation
The following example demonstrates how to offload a heavy computation to a worker and retrieve the result using a future. This ensures the main thread remains responsive.
import ballerina/io;
public function main() returns error ? {
// Define a future to hold the result of the worker
future string resultFuture;
// Start a worker to perform a CPU-intensive task
worker {
io:println("Worker: Starting heavy computation...");
// Simulate a heavy task
string processedData = "Processed Result";
// Assign the result to the future variable
resultFuture = processedData;
}
io:println("Main: Worker started, I am still responsive!");
// Wait for the worker to complete and retrieve the value
// This is a blocking call that waits for the future to be fulfilled
string finalValue = wait resultFuture;
io:println("Main: Received value: ", finalValue);
}
Managing Shared State with Locks
When multiple workers need to modify a shared variable, you encounter race conditions—where the final value depends on the unpredictable order of thread execution. To prevent this, use the lock keyword to ensure only one worker can access a critical section of code at a time.
import ballerina/io;
// Shared state
int sharedCounter = 0;
lock counterLock;
public function main() returns error ? {
worker {
lock counterLock {
sharedCounter += 1;
}
}
worker {
lock counterLock {
sharedCounter += 1;
}
}
// Give workers time to complete for this example
runtime:sleep(100);
io:println("Final Counter: ", sharedCounter);
}
Operational Constraints and Risks
- Context Isolation: Workers do not inherit the local variable scope of the main thread. Any data required by the worker must be passed as a parameter or accessed via shared global state.
- Resource Overhead: Each worker maps to an OS thread. Creating hundreds of workers simultaneously can lead to excessive context switching, increasing CPU overhead and memory consumption.
- Deadlocks: Using multiple
lockblocks across different workers can lead to deadlocks if two workers are waiting for locks held by each other.
Verification and Testing
To verify that a worker is actually running in parallel and not blocking the main loop, you can implement a simple timestamp or log check. If the "Main: Worker started" log appears before the worker's internal "Computation complete" log, the concurrency is functioning correctly.
To check for race conditions in a production-like environment, run the application with a high number of concurrent workers updating a single shared variable without a lock. If the final count is lower than the number of workers started, a race condition is confirmed, and a lock is required.
Rollback and State Recovery
Because workers operate on separate threads, a failure (panic) within a worker does not necessarily crash the main thread unless the main thread attempts to wait on a future that failed. To recover from worker failures, wrap the worker logic in a try-catch block to handle errors locally before they propagate to the future.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.