Parallelize Blocking API Calls with a Thread Pool
BasetenSoftware Engineer · Technical ScreenLast updated
Implement a robust, bounded concurrent batch runner that parallelizes blocking calls while preserving input order, handling errors, and enforcing timeouts.
Parallelize Blocking API Calls with a Thread Pool
In machine learning model serving and backend microservices, a common bottleneck is querying downstream services sequentially. When processing a batch of items (e.g., fetching embeddings, running inference, or querying external databases), blocking calls should run concurrently without overwhelming downstream services.
Implement a function execute_batch that processes a list of inputs using a bounded thread pool, preserving the original ordering of results while managing exceptions and timeouts.
Function Signature
from dataclasses import dataclass
from typing import Any, Callable, List, Optional, TypeVar
T = TypeVar("T")
R = TypeVar("R")
@dataclass
class TaskResult:
index: int
input: Any
value: Optional[Any] = None
error: Optional[Exception] = None
timed_out: bool = False
def execute_batch(
items: List[T],
fn: Callable[[T], R],
max_workers: int,
timeout: Optional[float] = None,
) -> List[TaskResult]:
pass
Requirements
- Worker Bounds: At most
max_workersthreads may invokefnconcurrently. - Order Preservation: The returned list of
TaskResultobjects must correspond exactly to the indices and order ofitems(result[i].index == i). - Failure Isolation: An unhandled exception in
fn(items[i])should not abort execution for remaining items. Store the caught exception inTaskResult.errorwithvalue = None. - Overall Timeout: If
timeout(in seconds) is provided, the total execution ofexecute_batchmust not exceedtimeout. Any task not completed within the deadline must havetimed_out = True(orerrorset to aTimeoutError). Pending tasks waiting in the pool queue should be canceled immediately once the timeout expires. - Resource Cleanup: Thread pool resources must be cleaned up properly before returning.
Examples
Example 1: Latency Variance with Preserved Order
import time
def mock_call(x: int) -> int:
# Later elements finish faster
time.sleep((5 - x) * 0.05)
return x * 10
results = execute_batch(items=[1, 2, 3, 4], fn=mock_call, max_workers=2)
assert [r.value for r in results] == [10, 20, 30, 40]
assert [r.index for r in results] == [0, 1, 2, 3]
Example 2: Errors and Timeouts
def faulty_call(x: int) -> int:
if x == 2:
raise ValueError("Invalid input")
if x == 3:
time.sleep(1.0)
return x
results = execute_batch(
items=[1, 2, 3],
fn=faulty_call,
max_workers=2,
timeout=0.2
)
assert results[0].value == 1
assert isinstance(results[1].error, ValueError)
assert results[2].timed_out is True
Constraints
0 <= len(items) <= 10001 <= max_workers <= 64timeoutis eitherNoneor a positive float.
Hints
Hint 1
Track the initial index of each item alongside its submitted future so you can populate a pre-allocated results list by index regardless of completion order.
Hint 2
Use `concurrent.futures.wait` with a computed deadline to handle overall batch timeouts and attempt `future.cancel()` on pending futures when time runs out.
Hint 3
Ensure `ThreadPoolExecutor.shutdown` is called with appropriate cancellation flags or managed via a context manager to prevent worker thread leaks.
Follow-up questions
- In Python, worker threads cannot be forcefully killed if `fn` is blocked in non-interruptible C code or socket I/O. How would you design client-side cancellation for unresponsive tasks?
- How would you refactor this interface to support streaming ordered results (as an iterator/generator) back to the caller as they finish, rather than waiting for the entire batch to complete?
- If downstream endpoints enforce strict rate limits (e.g., 50 requests per second across all threads), how would you integrate token-bucket rate limiting into this pool?
Solution walkthrough is for members
Get the full walkthrough with reference code and complexity analysis, plus Kitty Coach at 300 messages a day. Hints, the code runner and community answers stay free.