The Fix
Addresses a potential memory leak by breaking cyclic references when there is an exception handling a request.
Based on closed aio-libs/aiohttp issue #10548 · PR/commit linked
@@ -17,3 +17,6 @@ indent_style = tab
[*.{yml,yaml}]
indent_size = 2
+
+[*.rst]
+max_line_length = 80
from aiohttp import web
import gc
import asyncio
import tracemalloc
from time import time
import objgraph
gc.set_debug(gc.DEBUG_LEAK)
def get_garbage():
result = []
gc.collect()
for obj in gc.garbage:
obj_name = type(obj).__name__
result.append(f'{obj_name}')
if obj_name in ('Request',):
print('Request not collected!')
objgraph.show_backrefs(
obj,
max_depth=30,
too_many=50,
filename=f"/tmp/{int(time() * 1000)}err_referrers.png",
)
return result
async def hanlder(request):
print(f'read request')
req = await request.json()
return web.Response(text="Request has been receieved")
async def on_startup(app) -> None:
# asyncio.create_task(show_memory())
asyncio.create_task(show_objgraph())
async def show_objgraph():
while True:
await asyncio.sleep(10)
gc.collect()
print(f'Garbage objects: {get_garbage()}')
async def show_memory():
print('start tracing memory')
tracemalloc.start(25)
start = tracemalloc.take_snapshot()
snapshot_num = 1
while True:
await asyncio.sleep(20)
current = tracemalloc.take_snapshot()
# compare current snapshot to starting snapshot
stats = current.compare_to(start, 'filename')
print('top diffs since start')
# print top diffs: current snapshot - start snapshot
for i, stat in enumerate(stats[:15], 1):
print(f'top diffs: {i}, {str(stat)}')
traces = current.statistics('traceback')
for stat in traces[:2]:
for line in stat.traceback.format():
print(line)
snapshot_num = snapshot_num + 1
my_web_app = web.Application()
my_web_app.router.add_route('POST', '/image', hanlder)
my_web_app.on_startup.append(on_startup)
web.run_app(my_web_app)
Re-run the minimal reproduction on your broken version, then apply the fix and re-run.
Option A — Apply the official fix\nAddresses a potential memory leak by breaking cyclic references when there is an exception handling a request.\nWhen NOT to use: This fix should not be applied if the application relies on the cyclic references for functionality.\n\n
Why This Fix Works in Production
- Trigger: Memory Leak in web request due to cyclic reference
- Mechanism: Cyclic references in the Request object prevent garbage collection, leading to increased memory usage
- If left unfixed, this can cause silent data inconsistencies that propagate (bad cache entries, incorrect downstream decisions).
Why This Breaks in Prod
- Cyclic references in the Request object prevent garbage collection, leading to increased memory usage
- Production symptom (often without a traceback): Memory Leak in web request due to cyclic reference
Proof / Evidence
- GitHub issue: #10548
- Fix PR: https://github.com/aio-libs/aiohttp/pull/3462
- Reproduced locally: No (not executed)
- Last verified: 2026-02-09
- Confidence: 0.70
- Did this fix it?: Yes (upstream fix exists)
- Own content ratio: 0.43
Discussion
High-signal excerpts from the issue thread (symptoms, repros, edge-cases).
“looks like the other one I found isn't actually a problem it does become unreachable get cleaned up. closing this via https://github.com/aio-libs/aiohttp/pull/10571”
“While I can confirm reproduction, I have had no luck figuring out how to fix this as it seems its referenced in more than one…”
“Can you reproduce this on 3.11.x? We aren't shipping new builds of 3.10 anymore.”
“Doesn't seem to be reproducible on master”
Failure Signature (Search String)
- Memory Leak in web request due to cyclic reference
Copy-friendly signature
Failure Signature
-----------------
Memory Leak in web request due to cyclic reference
import tracemalloc
Error Message
Signature-only (no traceback captured)
Error Message
-------------
Memory Leak in web request due to cyclic reference
import tracemalloc
Minimal Reproduction
from aiohttp import web
import gc
import asyncio
import tracemalloc
from time import time
import objgraph
gc.set_debug(gc.DEBUG_LEAK)
def get_garbage():
result = []
gc.collect()
for obj in gc.garbage:
obj_name = type(obj).__name__
result.append(f'{obj_name}')
if obj_name in ('Request',):
print('Request not collected!')
objgraph.show_backrefs(
obj,
max_depth=30,
too_many=50,
filename=f"/tmp/{int(time() * 1000)}err_referrers.png",
)
return result
async def hanlder(request):
print(f'read request')
req = await request.json()
return web.Response(text="Request has been receieved")
async def on_startup(app) -> None:
# asyncio.create_task(show_memory())
asyncio.create_task(show_objgraph())
async def show_objgraph():
while True:
await asyncio.sleep(10)
gc.collect()
print(f'Garbage objects: {get_garbage()}')
async def show_memory():
print('start tracing memory')
tracemalloc.start(25)
start = tracemalloc.take_snapshot()
snapshot_num = 1
while True:
await asyncio.sleep(20)
current = tracemalloc.take_snapshot()
# compare current snapshot to starting snapshot
stats = current.compare_to(start, 'filename')
print('top diffs since start')
# print top diffs: current snapshot - start snapshot
for i, stat in enumerate(stats[:15], 1):
print(f'top diffs: {i}, {str(stat)}')
traces = current.statistics('traceback')
for stat in traces[:2]:
for line in stat.traceback.format():
print(line)
snapshot_num = snapshot_num + 1
my_web_app = web.Application()
my_web_app.router.add_route('POST', '/image', hanlder)
my_web_app.on_startup.append(on_startup)
web.run_app(my_web_app)
What Broke
Memory usage grows significantly during web requests, causing potential service degradation.
Why It Broke
Cyclic references in the Request object prevent garbage collection, leading to increased memory usage
Fix Options (Details)
Option A — Apply the official fix
Addresses a potential memory leak by breaking cyclic references when there is an exception handling a request.
Fix reference: https://github.com/aio-libs/aiohttp/pull/3462
Last verified: 2026-02-09. Validate in your environment.
When NOT to Use This Fix
- This fix should not be applied if the application relies on the cyclic references for functionality.
Verify Fix
Re-run the minimal reproduction on your broken version, then apply the fix and re-run.
Did This Fix Work in Your Case?
Quick signal helps us prioritize which fixes to verify and improve.
Prevention
- Track RSS + object counts after deployments; alert on monotonic growth and GC pressure.
- Add a long-running test that repeats the failing call path and asserts stable memory.
Related Issues
No related fixes found.
Sources
We don’t republish the full GitHub discussion text. Use the links above for context.