A process pool lets Python run independent work in separate processes. But logging across those processes requires more care than ordinary single-process logging: a normal file handler is not automatically safe for simultaneous writes by multiple workers. This lesson extends OSPython.038: Multiprocessing Logging to concurrent.futures.ProcessPoolExecutor.
Learning objectives
- Identify why direct multi-process writes to one log file are risky.
- Use a manager-backed queue to carry worker log records.
- Set up QueueHandler in pool workers and QueueListener in the parent process.
- Ensure clean startup, shutdown, and exception handling.
- Avoid queue recursion and inadvertent loss of records.
The process-pool logging problem
A ProcessPoolExecutor manages worker processes, often using spawn-compatible startup. Each worker has its own Python interpreter state and logger configuration. Passing a logger object into a worker does not establish a reliable shared logging destination. The Python Logging Cookbook recommends collecting records centrally rather than having separate processes write to the same ordinary file handler.
Corey Schafer explains Python loggers, handlers, and formatters—the building blocks used in the process-pool design.
A queue-based design
A common architecture uses one queue shared by the pool workers. Each worker attaches logging.handlers.QueueHandler(queue) to its logger. In the parent process, logging.handlers.QueueListener(queue, file_handler) consumes records and writes them through a single destination handler. This separates producing records from formatting and writing them.
For ProcessPoolExecutor, Python’s cookbook specifically illustrates use of a multiprocessing.Manager().Queue(), which can be shared with pool workers. Do not substitute a thread-only queue.Queue. Be cautious with multiprocessing.SimpleQueue, which the logging handlers documentation advises against for multiprocessing.
Minimal implementation steps
- Define a top-level
configure_worker(queue)function so spawn-based platforms can import it. - In that function, attach a
QueueHandlerto the intended application logger and set its level. - Create a
multiprocessing.Manager()in the parent and obtainmanager.Queue(). - Create a file or console handler in the parent, then construct and start
QueueListener. - Construct
ProcessPoolExecutor(initializer=configure_worker, initargs=(queue,)). - Submit jobs and consume their futures to surface exceptions.
- After the executor has shut down and workers have finished, stop the listener before closing the manager and its queue.
Illustrative code fragments
Parent process: with multiprocessing.Manager() as manager: followed by q = manager.Queue(), listener = QueueListener(q, StreamHandler()), and listener.start(). Then create ProcessPoolExecutor(initializer=configure_worker, initargs=(q,)). Stop the listener in a finally clause after the executor finishes. Worker initializer: logging.getLogger('app').addHandler(QueueHandler(q)) and logging.getLogger('app').setLevel(logging.INFO).
These fragments explain the configuration sequence rather than presenting a complete copy-paste program. A complete implementation must import the named classes, define the worker and initializer at module scope, and place process-pool creation under if __name__ == '__main__':. On Windows and spawn-based environments this guard is essential.
Corey Schafer covers logging levels, file output, and formatting—useful when choosing the destination handler for centralized process-pool logs.
Avoid duplicate and recursive logging
If the same logger already has a local StreamHandler or FileHandler, adding a QueueHandler can duplicate output. Review handler inheritance and propagation before configuration. The multiprocessing module has its own internal logger: routing its queue-operation DEBUG messages through a QueueHandler that writes to the same multiprocessing queue can cause recursion or deadlock. Keep the internal multiprocessing logger separate from the application queue pipeline.
What happens when workers fail?
A task exception is reported through its Future when you call future.result(); the logging pipeline is not a substitute for handling exceptions. Workers that terminate abruptly may lose queued messages that have not been delivered. Design shutdown order carefully, avoid relying on daemonized worker cleanup, and test crash behavior explicitly.
Developers discuss the challenges of concurrent process logging and the tradeoffs between locks, queues, and separate files.
Performance and reliability
Queue-based logging reduces contention at the destination, but queue serialization, inter-process communication, and formatting still consume resources. High-volume systems may need bounded queues, backpressure policy, or a dedicated logging service. A non-blocking enqueue can fail when a bounded queue is full; define whether dropping low-priority records is acceptable and monitor losses.
A Python community troubleshooting thread describes why logging configuration may not automatically carry across worker processes.
Hands-on exercise
- Create a pool with three workers, each logging its process name and task number.
- Route every worker’s messages through one manager-backed queue.
- Add a formatter containing timestamp, process name, log level, and message.
- Deliberately raise an exception in one task; capture it with
future.result(). - Verify that the listener shuts down cleanly and all completed tasks’ expected messages appear once.
Knowledge check and answers
- Why not use one FileHandler directly from all workers? It does not guarantee safe coordinated cross-process writes.
- Which handler sends records into a queue? QueueHandler.
- Which component consumes queued records? QueueListener.
- Why use Manager().Queue() in this pool example? It provides a queue object that can be shared with process-pool workers.
- Why call future.result()? To obtain results and surface worker exceptions.
- What is a queue-recursion hazard? Routing the multiprocessing internal logger’s queue DEBUG messages into the same queue.
Primary documentation
See the Python Logging Cookbook, QueueHandler and QueueListener documentation, and ProcessPoolExecutor reference. Python’s documentation provides a more complete tested example than the fragments in this lesson.
Related lessons
- OSPython.038: Multiprocessing Logging
- OSPython.037: QueueHandler and QueueListener
- OSPython.036: Logging Configuration
BitcoinVersus.Tech Editor’s Note:
We volunteer daily to ensure the credibility of the information on this platform is Verifiably True. If you would like to support to help further secure the integrity of our research initiatives, please donate using the QR code provided
For best QR-code scanning results, please scan this donation code from a desktop or larger screen. Mobile screens may prevent camera recognition.


Leave a Reply