Severe memory regression in the supervisor process when workers > 1 (0.47.0+) #2980
Unanswered
mehdiboubnan
asked this question in
Potential Issue
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
What I'm seeing
After upgrading uvicorn 0.46.0 → 0.47.0+ on a FastAPI app running under Kubernetes with
--workers 2, the parent (supervisor) process holds ~500 MB of RSS that it never serves with.Downgrading to 0.46.0 immediately fixes it.
Setup
UVICORN_WORKERS=2, no--reloadspawnon Linux for Python 3.14-track; uvicorn also forces spawn explicitly viauvicorn/_subprocess.py)Numbers (py-spy + /proc inside the pod)
threading.Event.wait)py-spy
dump --pid 1shows the supervisor parked inMultiprocess.run → should_exit.wait(0.5). It doesn't handle requests, yet it holds nearly as much memory as a worker.Root cause (what I believe is happening)
#2919 (b499bc4, released 0.47.0) added an eager
config.load_app()inuvicorn.run()so the parent imports the ASGI app before constructingServer. The motivations were good (fail-fast on bad app paths, sidestep the asyncio-in-module-body crash from #941).But:
get_subprocessforcesmultiprocessing.get_context("spawn").sys.modules.Net effect: every module imported by
load_app()in the parent is pure resident-memory overhead for the lifetime of the server, never inherited, never used. For non-trivial apps it adds up to hundreds of MB.Why I think it's worth fixing
This silently regressed memory for anyone running uvicorn with
workers > 1and a non-trivial app. The HPA in our cluster startedscaling pods more aggressively because the per-pod baseline jumped. The CodSpeed benchmark added in the same PR (#2919) used a trivial app, so it couldn't catch this.
Possible fix (happy to draft a PR if maintainers agree)
Only execute the app body in the parent when the parent is the worker (single process, no reload). For
workers > 1or--reload, validate the import string viaimportlib.util.find_spec(which doesn't execute the module body). That keeps the fail-fast property for missing modules and still avoids the asyncio-in-module-body crash for the single-process path, while making the supervisor cheap again.Note:
find_specstill imports the target's parent packages, so apps whose top-level__init__.pyis heavy will see some residual supervisor RSS. For most apps the parent__init__.pyis empty or near-empty; the patch is a strict improvement even when it isn't.Beta Was this translation helpful? Give feedback.
All reactions