You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Hypercorn 0.18.0 does not work with Python 3.14. Several asyncio APIs that Hypercorn uses (asyncio.TaskGroup, asyncio.timeout, asyncio.wait_for) now require a proper task context in 3.14, but Hypercorn's asyncio.Runner doesn't establish one.
Environment
Hypercorn 0.18.0
Python 3.14.4 (Alpine Linux, GCC 15.2.0)
Running with --workers 2 --worker-class asyncio
Errors
On startup, Hypercorn crashes with errors like:
RuntimeError: asyncio.TaskGroup requires a running event loop with a current task
and
RuntimeError: timeout() requires a running event loop with a current task
These come from worker_serve() in hypercorn/asyncio/run.py and the lifespan handling in hypercorn/asyncio/lifespan.py.
Root cause
Hypercorn's _run() function uses asyncio.Runner.run() which runs the coroutine directly via loop.run_until_complete(). Python 3.14 now enforces that TaskGroup, timeout(), and wait_for() have a proper asyncio.current_task() context. Since Runner doesn't wrap work in an explicit task the way asyncio.run() does internally, these APIs fail.
Additionally, on Python 3.14 (Alpine/GCC build specifically), asyncio.current_task() returns None in daemon threads even after explicit loop.create_task() + loop.run_until_complete(). This appears to be a CPython bug with the C-level task registry, but it means any code in Hypercorn workers that spawns daemon threads with their own event loops (e.g., background task executors using httpx.AsyncClient) will also break, since anyio and sniffio depend on current_task() being non-None.
Workaround
We are currently monkey-patching three things to run Hypercorn 0.18.0 on Python 3.14:
_run - replaced with a version that wraps work in asyncio.run() instead of using Runner directly, establishing a proper task context
worker_serve - replaced with a version that avoids TaskGroup and asyncio.timeout, using asyncio.wait() and asyncio.ensure_future() instead
asyncio.wait_for - globally replaced with a compatible implementation using asyncio.wait() + asyncio.ensure_future()
The full patch is ~300 lines. Happy to share it or submit a PR if that would be helpful.
Suggested fix
The minimal fix would be to ensure _run() wraps the main coroutine in an explicit asyncio.Task (e.g., via asyncio.run() instead of Runner.run()), so that asyncio.current_task() returns a valid task throughout Hypercorn's lifecycle.
Additional issue: shutdown task cleanup in the worker_serve workaround
We discovered another issue related to the Python 3.14 compatibility workaround described above.
The original worker_serve shutdown logic (and our initial patched version) waits for in-flight server tasks to complete within the graceful timeout, but never cancels remaining tasks after the timeout expires. On Python 3.14, this causes orphaned Task objects to be garbage-collected while still pending, triggering:
Task was destroyed but it is pending!
task: <Task pending name='Task-10049' coro=<_worker_serve_patched.<locals>._server_callback() running at ...>
This is logged as an error by asyncio's default exception handler.
Fix: After the graceful timeout, explicitly cancel any unfinished server tasks and await them before proceeding to lifespan shutdown:
ifserver_tasks:
gathered_server_tasks=asyncio.gather(*server_tasks)
try:
awaitasyncio.wait(
{asyncio.ensure_future(gathered_server_tasks)},
timeout=config.graceful_timeout,
)
exceptException:
pass# Cancel any tasks that didn't finish within the graceful timeoutfortaskinserver_tasks:
ifnottask.done():
task.cancel()
awaitasyncio.gather(*server_tasks, return_exceptions=True)
This may also affect the upstream worker_serve implementation (not just the workaround), since the original code has the same pattern of waiting with a timeout but not cancelling afterward.
Activity
seidnerj commentedon Apr 12, 2026
Additional issue: shutdown task cleanup in the
worker_serveworkaroundWe discovered another issue related to the Python 3.14 compatibility workaround described above.
The original
worker_serveshutdown logic (and our initial patched version) waits for in-flight server tasks to complete within the graceful timeout, but never cancels remaining tasks after the timeout expires. On Python 3.14, this causes orphanedTaskobjects to be garbage-collected while still pending, triggering:This is logged as an error by asyncio's default exception handler.
Fix: After the graceful timeout, explicitly cancel any unfinished server tasks and await them before proceeding to lifespan shutdown:
This may also affect the upstream
worker_serveimplementation (not just the workaround), since the original code has the same pattern of waiting with a timeout but not cancelling afterward.m-u-xyz commentedon Jul 1, 2026
Another possible workaround: use https://github.com/davidbrochart/anycorn .