Skip to main content

The Solo Pool: Inline Execution

The Solo pool is the simplest execution model in Celery, designed for environments where tasks should run synchronously within the main worker process. Unlike the prefork or eventlet pools, which manage multiple child processes or green threads, the Solo pool executes tasks inline, meaning the worker process itself performs the work.

Synchronous Execution Mechanism

The core of the Solo pool's implementation is its mapping of task application to a direct function call. In celery.concurrency.solo.TaskPool, the on_apply method—which is the entry point for executing a task—is assigned directly to the apply_target helper function.

class TaskPool(BasePool):
"""Solo task pool (blocking, inline, fast)."""

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.on_apply = apply_target
self.limit = 1
signals.worker_process_init.send(sender=None)

When the worker receives a task and calls pool.apply_async, the BasePool implementation (found in celery/concurrency/base.py) invokes self.on_apply. In the Solo pool, this triggers apply_target immediately:

def apply_target(target, args=(), kwargs=None, callback=None,
accept_callback=None, pid=None, getpid=os.getpid,
propagate=(), monotonic=time.monotonic, **_):
# ... (setup)
try:
ret = target(*args, **kwargs)
except Exception:
raise
# ... (error handling)
else:
callback(ret)

Because apply_target executes target(*args, **kwargs) in the current thread, the worker is effectively "captured" by the task until it completes.

Process and Concurrency Model

The Solo pool enforces a strict concurrency limit of 1. This is reflected in the _get_info method, which reports the worker's own PID as the only active process:

def _get_info(self):
info = super()._get_info()
info.update({
'max-concurrency': 1,
'processes': [os.getpid()],
'max-tasks-per-child': None,
'put-guarded-by-semaphore': True,
'timeouts': (),
})
return info

This design choice simplifies the worker architecture significantly. There is no inter-process communication (IPC) overhead, no serialization of results between processes, and no management of a process pool. However, it also means that the --concurrency (or -c) command-line argument is ignored when using the Solo pool.

Signal Handling and Initialization

A notable detail in the Solo pool's implementation is how it handles the worker_process_init signal. In multiprocess pools, this signal is typically sent inside a newly forked child process to allow for per-process setup (like re-establishing database connections).

In celery/concurrency/solo.py, the signal is sent during the TaskPool initialization:

signals.worker_process_init.send(sender=None)

Since the Solo pool runs in the main process, this signal is emitted once when the pool is created. This ensures that any signal handlers expecting a "worker process" environment are still executed, maintaining compatibility with tasks that rely on this initialization hook.

Design Tradeoffs and Use Cases

The Solo pool is a specialized tool with specific advantages and risks:

  • Debugging: Because tasks run in the same process as the worker, developers can use standard debuggers (like pdb) without dealing with the complexities of multi-process debugging.
  • Resource Constraints: In environments where fork() is unavailable (such as certain restricted containers or Windows environments) or where the overhead of multiple processes is too high, the Solo pool provides a lightweight alternative.
  • Blocking Risk: The primary tradeoff is that the worker is completely blocked while a task is running. During this time, the worker cannot process heartbeats, respond to remote control commands (like inspect), or acknowledge new tasks from the broker. If a task hangs, the entire worker hangs.
  • Performance: For very short-lived tasks, the Solo pool can be faster than the prefork pool because it avoids the overhead of passing data across process boundaries.

To use the Solo pool, the worker must be started with the --pool=solo option, which causes the worker to resolve the implementation via celery.concurrency.get_implementation.