Skip to content

Clear exc.__traceback__ after handled exceptions to prevent memory leak#3205

Open
baconcheese113 wants to merge 1 commit into
Kludex:mainfrom
baconcheese113:fix/traceback-cleanup
Open

Clear exc.__traceback__ after handled exceptions to prevent memory leak#3205
baconcheese113 wants to merge 1 commit into
Kludex:mainfrom
baconcheese113:fix/traceback-cleanup

Conversation

@baconcheese113

Copy link
Copy Markdown

Summary

  • When wrap_app_handling_exceptions catches and successfully handles an exception, exc.__traceback__ retains a reference to the frame's locals, which include the ASGI send/receive closures and per-request state
  • If the exception object is long-lived (e.g., a module-level HTTPException reused across requests — common in auth libraries), this pins per-request objects in memory permanently. GC cannot collect these because the module holds a strong non-cyclic reference to the exception
  • The fix adds exc.__traceback__ = None after the handler runs and the response is sent. Unhandled exceptions (raise exc) are not affected — their traceback is preserved for propagation

Reference chain that causes the leak

exception.__traceback__
  → frame (wrapped_app's except block)
    → locals:
      → sender (closure) → captured `send` → RequestResponseCycle (~19 KB)
      → receive → RequestResponseCycle

Precedent

Test plan

  • Reproduced the bug on unpatched code (shared exception's __traceback__ is not None after handling)
  • Confirmed the fix resolves it (__traceback__ is None after handling)
  • Verified traceback doesn't accumulate across 100 requests
  • Verified unhandled exceptions (no handler found → raise exc) still preserve their traceback
  • New test test_handled_exception_clears_traceback passes on both asyncio and trio
  • Full test suite: 925 passed, 2 xfailed, 0 failures

…ry leak

When `wrap_app_handling_exceptions` catches and handles an exception,
the traceback retains references to the ASGI send/receive closures and
per-request state. If the exception object is long-lived (e.g. a
module-level HTTPException reused across requests), this pins
per-request objects in memory permanently, causing a leak that GC
cannot collect.

Clear `exc.__traceback__` after the handler runs and the response is
sent. Unhandled exceptions (re-raised via `raise exc`) are not affected.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants