skip to content

How do you enable asyncio debug mode, and what does its slow-callback warning tell you?

level: juniorimportance: should knowfreq 35%

answer

  1. Three ways to switch it on
  2. One of them needs no code change
  3. The loop times every callback it runs
  4. Default threshold is a tenth of a second
  5. The warning names the handle and its duration

basics

~20 s

Enable it with asyncio.run(main(), debug=True), the PYTHONASYNCIODEBUG environment variable, or the -X dev flag. The loop then logs a warning whenever one callback or task step holds it longer than 0.1 seconds — the fingerprint of blocking code.

solid answer

~50 s

There are three switches: `asyncio.run(main(), debug=True)`, the `PYTHONASYNCIODEBUG` environment variable set to a non-empty value, or running the interpreter with `-X dev`. Once debug mode is on, the loop times every callback and every step of a task, and logs a warning on the `asyncio` logger when one exceeds the slow_callback_duration threshold, which defaults to 0.1 seconds and is settable on the loop. The message names the handle and the elapsed time — `Executing <Task ...> took 0.501 seconds` — and it almost always means synchronous work ran inside a coroutine, because a single-threaded loop cannot preempt a step that never awaits. Debug mode also records where tasks and coroutines were created so warnings point at a source line and checks that loop APIs are called from the loop's own thread. It costs measurable overhead, so turn it on in development or on one instance rather than everywhere.

code

python · 10 lines
python
import asyncio
import time


async def parse_chunk():
    time.sleep(0.5)  # a synchronous call standing in for CPU-bound parsing


asyncio.run(parse_chunk(), debug=True)
# Executing <Task finished name='Task-1' coro=<parse_chunk() ...>> took 0.501 seconds

go deeper

for a junior

Recall the three switches — debug=True on asyncio.run, the PYTHONASYNCIODEBUG environment variable, and -X dev — and that the warning means something held the loop, not that a remote call was slow.

for a middle

Explain the mechanics: the loop clocks each callback and task step against a 0.1 second threshold, a step is the code between two await points, and a single-threaded loop cannot interrupt one.

for a senior

Show how you use it in a real system: which instance runs with debug mode on, how you tune the threshold, and how you go from the warning's handle to the actual synchronous call behind it.

for a principal

Own the tradeoff between observability and overhead — whether blocking detection belongs in a permanently on, cheap in-process signal rather than a debug flag someone must remember to set on the day of the incident.

## What debug mode is asyncio has a per-loop debug flag. It is off by default because the checks it adds cost time and memory on every task creation and every loop iteration. Turning it on changes nothing about *what* your program does — it changes how much the runtime is willing to tell you. Three ways to switch it on, and they are equivalent: - `asyncio.run(main(), debug=True)` — explicit, per entry point. `asyncio.Runner` takes the same keyword. - `PYTHONASYNCIODEBUG=1` in the environment — no source change, which matters when you want it on a single container or a single CI job. - `-X dev` — the interpreter's development mode, which enables asyncio debug mode as one of the things it turns on. Inside a running loop you can also flip it by calling `set_debug(True)` on the loop object, and read it back with `get_debug()`. ## What it actually adds **Slow-callback warnings.** In debug mode the loop wraps each callback and each step of a task with a clock. If the elapsed time exceeds the loop's slow_callback_duration — default `0.1` seconds — it logs a warning on the logger named `asyncio`: ``` Executing <Task finished name='Task-1' coro=<parse_chunk() done, defined at app.py:5> ...> took 0.501 seconds ``` That is the single most useful line asyncio will ever print you. A step is the run of a coroutine between two `await` points. The event loop has exactly one thread and no way to interrupt a step; whatever that step does, the whole loop waits. So a step measured in hundreds of milliseconds means synchronous work ran where it should not have: a CPU-bound parse, a synchronous database or HTTP client, a blocking file read, name resolution, or a first-use import that pulled in a large package. **Creation tracebacks.** Debug mode attaches the source traceback of where a coroutine or task was created to the object. Warnings that would otherwise say only "a coroutine was never awaited" or "task exception was never retrieved" now come with the file and line where the object came from. **Thread-affinity checks.** Loop APIs that are not thread-safe raise instead of silently corrupting loop state when called from the wrong thread. **Louder resource warnings.** Unclosed transports and event loops surface as warnings rather than passing unnoticed. ## Reading the warning correctly The most common misreading is to blame the thing that appeared slow. The warning is not about a slow *service*; awaiting a slow service costs the loop nothing, because the task is suspended and the loop runs other work. The warning is about a slow *callback* — code that held the thread. Those are opposite diagnoses: one is upstream latency, the other is your own code. The threshold is a knob, not a law. `0.1` seconds is generous for a loop serving many concurrent connections; tightening it to `0.05` on a development machine surfaces things a production threshold hides. Tightening it too far produces noise from legitimate startup work. What debug mode will not tell you: it names the handle whose execution was slow, not the exact line inside it. If that handle belongs to a library, you have the entry point and no more. From there the routine is to reproduce with a lower threshold and narrow the suspect call. ## Cost, and where to run it Every task creation captures a traceback; every iteration takes extra clock readings and checks. On a busy loop that is real overhead, and the extra warnings are noisy in aggregated logs. The habit that works is: debug mode on by default in development and in tests, and in production either off or enabled on a canary instance behind an environment variable — which is exactly why `PYTHONASYNCIODEBUG` exists as an environment switch rather than only as a code flag. One caveat about the warning as a monitoring signal: it fires *after* the slow callback finished. It tells you the loop was blocked, and for how long, but you learn it retrospectively. If you need to know that the loop is stalled *right now*, you need something that measures the loop from inside while it runs, not a log line after the fact.

  • The warning fires but names a handle inside a library you do not control. What do you do next?
    The handle repr carries the coroutine and, in debug mode, where it was created, so you have the entry point. Reproduce with the slow-callback threshold lowered so the warning fires earlier and more often, then narrow the suspect synchronous call inside that entry point — typically a blocking client, a large parse, a filesystem read, or an import happening on first use.
  • Why should you not leave asyncio debug mode on in production by default?
    It captures a traceback at every coroutine and task creation and adds per-iteration checks and clock readings, so a busy loop pays real overhead, and the extra warnings are noisy in aggregated logs. The usual compromise is debug mode always on in development and tests, and in production enabled through the environment variable on one canary instance.
  • Does the slow-callback warning mean the service the task was calling is slow?
    No, and that is the classic misreading. Awaiting a slow service costs the loop nothing: the task is suspended and the loop runs other work. The warning measures time spent *inside* a callback or task step, so it points at synchronous code holding the loop's only thread, which is a completely different diagnosis from upstream latency.

It is a speed camera on the event loop's single lane: it does not stop anyone, it just photographs whoever occupied the lane longer than a tenth of a second.

saying these in an interview costs you the question

  • Thinks debug mode makes asyncio run faster
  • Reads the slow-callback warning as upstream service latency
  • Cannot name a single way to turn debug mode on
  • Assumes the event loop can preempt a long-running callback
  • Says debug mode is free and should always be enabled
  • Believes the warning pinpoints the exact blocking line

context