A thread pool executor runs submitted callables using a reusable group of worker threads. It is especially useful when blocking APIs, such as requests, would otherwise stop an asyncio event loop from doing other work. section 2.7.2 section 7.2.2
Use a thread pool executor to:
- Distribute blocking I/O tasks across worker threads instead of executing them serially. section 7.2.2
- Connect blocking functions to asyncio with loop.run_in_executor and await their results with asyncio.gather. section 7.2.3
- Control concurrency with the pool rather than creating one thread for every task; the default worker count is min(32, os.cpu_count() + 4). section 7.2.2
Running many blocking HTTP requests
A ThreadPoolExecutor can map get_status_code across 1,000 URLs. The threaded version can finish in roughly 8–9 seconds on an eight-core machine with a fast connection, compared with more than 100 seconds synchronously. Setting max_workers to 1,000 is shown as an alternative, but the default pool avoids creating a surprising number of threads. section 7.2.2
Listing 7.5 Using a thread pool executor with asyncio
import functools
import requests
import asyncio
from concurrent.futures import ThreadPoolExecutor
from util import async_timed
def get_status_code(url: str) -> int:
response = requests.get(url)
return response.status_code
@async_timed()
async def main():
loop = asyncio.get_running_loop()
with ThreadPoolExecutor() as pool:
urls = ['https:// www .example .com' for _ in range(1000)]
tasks = [loop.run_in_executor(pool, functools.partial(get_status_code, url)) for url in urls]
results = await asyncio.gather(*tasks)
print(results)
asyncio.run(main())
This runnable pattern creates a pool, submits blocking requests through loop.run_in_executor, and gathers their results while other asynchronous code can continue. section 7.2.3









