Skip to content

Conversation

@nascheme
Copy link

@nascheme nascheme commented Dec 18, 2025

Summary

This avoids creating reference cycles that can result in significant extra memory usage. The cyclic GC should clean up the cycles but avoiding them is better.

The Python 3.14.2 release makes the cyclic GC less aggressive about freeing reference cycles and so the cycles created from the _response attribute can result is significant memory use (80 MB vs 420 MB). With this change, my example script goes to using 36 MB. See this CPython bug.

Checklist

  • I understand that this PR may be closed in case there was no previous discussion. (This doesn't apply to typos!)
  • I've added a test for each change that was introduced, and I tried as much as possible to make a single atomic change.
  • I've updated the documentation accordingly.

This change is pretty small so I didn't add a test case for it. The _response attribute doesn't appear to be used from outside the Bound* classes. Should be no documentation update needed.

This avoids creating reference cycles that can result in significant
extra memory usage.  The cyclic GC should clean up the cycles but
avoiding them is better.
@karpetrosyan
Copy link
Contributor

Thank you both for investigating this!
Could you clarify which specific script I can run locally to observe the difference? I tried some of the scripts from the ticket, but they seem to be Linux-specific, so I can’t run them on my Mac.

@sergey-miryanov
Copy link

I use this one to test on windows. It uses psutil to measure rss.

@karpetrosyan
Copy link
Contributor

I see that before it grows from 51 to 84 MB, and afterward it only grows from 51 to 61 MB and then stabilizes (or grows extremely slowly).

@karpetrosyan
Copy link
Contributor

Is the first 50–60 MB spike unrelated, given that it still happens with this PR?

@sergey-miryanov
Copy link

I believe it is normal now, but I would like to hear @nascheme opinion.

@nascheme
Copy link
Author

The exact pattern of how process memory changes is going to be platform dependent. E.g. it depends on how eager the malloc is to release memory back to the OS when it is freed. It also depends on how much memory the ssl.create_default_context() consumes. On my Linux desktop, it's about 800 kB, which is kind of surprising. Script below to measure.

The important part is avoiding the reference cycle. I could add a unit test to confirm that if you wish but I think it's pretty apparent that the ._response attribute does create a cycle and that using a weakref will avoid that. If you want to see the memory usage go higher, either disable the GC, using gc.disable(), or set the thresholds higher.

# ssl_context_size.py

import ssl

def get_rss():
    import psutil

    p = psutil.Process()
    mem_info = p.memory_info()
    vms = mem_info.vms
    rss = mem_info.rss
    return rss / 1024.0

def main():
    rss = get_rss()
    ctxs = []
    N = 1_000
    for i in range(N):
        ctxs.append(ssl.create_default_context())
    rss2 = get_rss()
    size = (rss2 - rss) / N
    print(f'size {size} kB')

if __name__ == '__main__':
    main()

@karpetrosyan
Copy link
Contributor

LGTM! Thanks @nascheme and @sergey-miryanov. Would you be able to also update the changelog?

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.

3 participants