Prompt and context
An async service often satisfies requests from an in-memory cache without doing I/O, yet every coroutine still becomes a task and waits for event-loop scheduling. The team proposes setting asyncio.eager_task_factory globally. Evaluate the trade-off and propose a safe rollout.
Python documents that eager execution starts a coroutine synchronously while its Task is constructed; the Task is scheduled only when the coroutine blocks. The API was added in Python 3.12 and changes observable scheduling semantics.
What the interviewer is testing
The key is distinguishing synchronous completion from asynchronous blocking and explaining effects on fairness, ordering, exception timing, cancellation, and TaskGroup. Strong answers restrict the optimization to measured boundaries and include version checks, feature flags, rollback, and event-loop metrics.
Clarifying questions to ask first
- What is the minimum Python version across every deployment?
- Are these coroutines memory-cache reads, or can they perform network, database, file, or lock I/O?
- Does code depend on task-creation order, loop fairness, or
call_soonordering? - Are failure, cancellation, and deadlines owned by a
TaskGroupor request scope? - Is the target task-creation CPU, tail latency, or throughput, and what is the baseline?
30-second answer framework
“I would not enable it globally first. An eager factory starts a coroutine during Task construction, so a cache hit can avoid one event-loop scheduling turn; a blocking coroutine is scheduled normally. That changes ordering, exception timing, and fairness: a loop creating many synchronous tasks can delay timers and other requests. I would check the Python version, roll out at measured cache call sites, compare event-loop lag and tail latency, and keep the default factory as an immediate rollback.”
Step-by-step deep dive
Step 1: State the default and eager models
With the default factory, create_task schedules the coroutine to run soon. With an eager factory, construction immediately advances it until it returns, raises, or reaches its first blocking await. A synchronous completion may never enter the event-loop queue.
loop.set_task_factory(asyncio.eager_task_factory)
task = asyncio.create_task(read_cached(key))Therefore eager execution is an observable semantic change, not merely a faster scheduler.
Step 2: Choose the coroutine boundary
Good candidates are short, high-hit-rate memory-cache or memoized operations. Coroutines that may perform network, database, file, lock, or unbounded CPU work should keep a predictable blocking boundary so construction cannot monopolize the current task.
Step 3: Analyze order and fairness
When tasks are created in a loop, eager coroutines can complete immediately in creation order, changing an interleaving that previously depended on the event loop. A large synchronous batch can delay timers, I/O callbacks, and other requests. Track event-loop lag and bound batch size.
Step 4: Handle exceptions and cancellation
An exception raised before the first block can surface near create_task, changing the capture point and stack shape. Keep Task references, and let CancelledError propagate after cleanup. Never use eager mode to turn request cancellation or a cache failure into a successful response.
Step 5: Combine it with TaskGroup and deadlines
TaskGroup still owns the task tree, sibling cancellation, and join, but a child may already have completed or failed during create_task. Use structured try/except* handling and one outer deadline; do not assume every child first waits in a queue.
async with asyncio.TaskGroup() as group:
user = group.create_task(read_cached("user"))
orders = group.create_task(read_remote("orders"))Step 6: Check versions and scope the feature
The factory is available from Python 3.12. Python 3.14 also exposes an eager_start option on create_task. A multi-version service should validate the runtime at startup and prefer a local call-site experiment over an unconditional global factory change.
Step 7: Roll out with rollback metrics
Start at cache-only call sites behind a switch. Compare CPU, task-construction time, cache-hit tail latency, event-loop lag, exception rate, cancellation rate, and downstream QPS against the default factory. If fairness or errors regress, restore the default without changing business code.
Step 8: Test semantics, not only benchmarks
Cover synchronous return, synchronous raise, first await, external cancellation, multiple TaskGroup failures, deadlines, recursive task creation, timer fairness, and mixed cache/remote batches. Record events and assert ordering; a throughput benchmark alone cannot validate scheduling semantics.
Model high-quality answer
“Eager execution fits short, predictable cache coroutines because it removes a scheduling turn; it is risky for unpredictable I/O or long CPU paths. The main risk is changed ordering, fairness, and exception timing. I would check versions, enable it locally behind a flag, retain the default rollback, and monitor event-loop lag, tail latency, cancellation, and errors. TaskGroup and a shared deadline remain in place, as do normal cancellation and cleanup.”
Common mistakes
- Treating eager mode as semantics-free → ordering and exception timing change → write down both execution models and measure.
- Enabling it for every coroutine → slow I/O or CPU blocks the current task → canary only short synchronous paths.
- Ignoring Python versions → older deployments fail at startup → validate the runtime and retain the default factory.
- Looking only at average throughput → fairness silently regresses → add loop-lag and tail metrics.
- Assuming TaskGroup is unchanged → construction-time failures are mishandled → test eager child failures and exception groups.
- Swallowing cancellation or cleanup errors → requests leak work → preserve cancellation and verify release paths.
Follow-up questions and strong responses
Follow-up 1: Does a synchronous cache hit still leave a Task?
The Task may already be complete during construction. Do not require an event-loop turn; read the returned object and test the eager-completion path explicitly.
Follow-up 2: Does eager mode guarantee creation order for completion?
No. It changes start timing only; once a coroutine blocks, normal scheduling applies. If business order matters, coordinate explicitly or sort results by an operation key.
Follow-up 3: How do you prevent a cache-hit batch from starving requests?
Bound the batch, yield between batches when appropriate, and watch loop lag. Applying eager mode to one low-cost call site is safer than a global policy.
Follow-up 4: How do you roll it back after a regression?
Disable the switch, restore the default task factory, verify metrics return to baseline, and retain traces containing runtime version and event order for diagnosis.
Follow-up 5: How does eager_start relate to the factory?
eager_start is an explicit option for one Task; the factory is an event-loop default policy. Confirm the exact Python-version signature and precedence before combining them.