Python - Single thread executor already being used, would deadlock
Interview Questions practice on Codemia
Over 8,000 real interview questions from top companies, searchable by company and role.
Introduction
The error "cannot schedule new futures after shutdown" or a deadlock with ThreadPoolExecutor(max_workers=1) occurs when a task submitted to a single-threaded executor submits another task to the same executor and waits for it. Since there is only one worker thread, the first task occupies it while waiting for the second task to complete, but the second task cannot start because the only thread is blocked. This is a classic deadlock pattern. The solutions are: increase the thread pool size, avoid nested submit().result() calls, or restructure the code to eliminate the dependency.
The Deadlock Pattern
Task A is running on the single worker thread. It submits Task B to the same executor and calls .result(), blocking until Task B finishes. But Task B is queued and cannot start because the only thread is occupied by Task A, which is waiting for Task B.
Fix 1: Increase Thread Pool Size
With 2 or more workers, Task B can run on a different thread while Task A waits. However, if nesting goes deeper (Task B submits Task C), you need even more threads. This approach does not scale for arbitrary nesting depths.
Fix 2: Avoid Nested Submissions
Submitting tasks sequentially from the main thread avoids the deadlock because each task completes before the next is submitted. The executor's single thread is never simultaneously occupied by two dependent tasks.
Fix 3: Use asyncio Instead
asyncio uses cooperative multitasking — await suspends the coroutine without blocking a thread, so nested calls naturally work. For I/O-bound workloads, asyncio is a better fit than thread pools.
Fix 4: Separate Executor for Nested Tasks
Using separate executors for different levels of task nesting avoids the deadlock because each level has its own thread pool.
The Same Issue with ProcessPoolExecutor
ProcessPoolExecutor has the same deadlock potential plus the additional restriction that you cannot easily pass the executor to child processes (it is not picklable).
Common Pitfalls
- Calling
.result()on a future from inside the same executor: This is the direct cause of the deadlock. The calling task holds the thread, and the awaited task needs a thread from the same pool. Either increase pool size, submit sequentially from outside, or use a different executor. - Assuming
max_workers=1behaves like a queue: A single-thread executor does serialize tasks, but only when tasks are independent. If Task A depends on Task B (which is also submitted to the executor), the serialization becomes a deadlock because A cannot complete until B runs, and B cannot start until A completes. - Using executor inside a callback or decorator: If a function wrapped in
executor.submit()internally calls anotherexecutor.submit()(e.g., through a decorator or middleware), the nesting is hidden and the deadlock is not obvious from reading the top-level code. Audit the call chain for hidden submissions. - Not shutting down executors properly: Forgetting
executor.shutdown(wait=True)or using the executor as a context manager (with ThreadPoolExecutor() as e:) can leave orphaned threads. A deadlocked executor will hang on shutdown forever — add timeouts to.result()calls to detect deadlocks. - Increasing max_workers without bound to "fix" nesting: While more workers prevent the immediate deadlock, each additional nesting level requires another thread. For deep nesting or recursive patterns, the thread count grows unboundedly. Restructure the code to eliminate nesting instead.
Summary
- A single-thread executor deadlocks when a running task submits and waits for another task on the same executor
- Fix by increasing
max_workers, chaining tasks sequentially from outside the executor, or using separate executors - Use
asynciofor I/O-bound workloads — cooperativeawaitdoes not block threads - Add timeouts to
.result(timeout=N)to detect and recover from deadlocks - Audit the full call chain to find hidden nested
executor.submit()calls - Avoid recursive or deeply nested submissions — restructure as sequential task chains
Related reading
- Python 3.6 async aioodbc blocking
- Python 3 How to submit an async function to a threadPool?
- Python + Distributed - Is it possible using Dask to utilize a set of workers to apply a function to seperate files from a folder concurrently
- Python async and CPU-bound tasks?
- Python - sklearn How to pass parameters to the customize ModelTransformer class by gridsearchcv
- Python - Speed up an A Star Pathfinding Algorithm
- Python Asynchronous Reverse DNS Lookups
- Python asyncio context
.png&w=3840&q=75)
Tackling System Design Interview Problems
A short course that equips you with the skills to approach system design interviews methodically.
Start the free courseTrack what you have practised
A free account saves your progress, solutions and study plan across every problem on Codemia.
Interview Questions practice on Codemia
Over 8,000 real interview questions from top companies, searchable by company and role.