Skip to content

Importing adcp.client / adcp.server costs ~520 MiB RSS on 9.x (3.4x 6.6): adcp.types._eager loads the full type surface #1694

Description

@idemidenko

Summary

Importing adcp.client, adcp.server.test_controller, adcp.decisioning or adcp.compat.versions loads the whole generated type surface via adcp.types._eager. Since 9.x that costs about 520 MiB of resident memory, 3.4 times what 6.6 cost. A seller agent that ran comfortably in a 512Mi container on 6.6 is OOM-killed at startup on 9.3.

Measurements

Each figure is peak RSS (ru_maxrss) of a fresh python -I -c "import <module>", measured on Python 3.12.6 with no other packages.

module 6.6.0 9.3.0 10.0.0b1
adcp 16 MiB 17 MiB 17 MiB
adcp.types._eager 153 MiB 521 MiB 504 MiB
adcp.client 200 MiB 580 MiB 565 MiB
adcp.server.test_controller 187 MiB 576 MiB 562 MiB

What adcp.types._eager loads:

6.6.0 9.3.0 10.0.0b1
Pydantic model classes defined 1,965 3,224 3,347
...of which have a built validator 418 351 355
adcp.* modules imported 716 1,128 1,175

Notes:

  • The model count grew by 1.6x, but memory grew by 3.4x, so the cost per class roughly doubled as well.
  • defer_build=True already keeps most validators unbuilt. What remains is defining the classes themselves: FieldInfo objects, core-schema dicts and typing objects.
  • tracemalloc attributes only about 174 MiB of the 9.3 import to the Python heap. The rest is outside it, probably in pydantic-core.
  • In a real seller (an MCP server on fastmcp built on adcp.server, which also uses ADCPClient for outbound governance), memory goes from 248 MiB on 6.6 to about 660 MiB on 9.3 inside a container.

Why an adopter can't avoid it

Importing flat names from their defining modules instead of adcp.types helps only until any SDK module that needs _eager is imported. adcp.client, adcp.server.test_controller, adcp.decisioning.state_machines and adcp.compat.versions each do. In a seller that uses governance (ADCPClient) or the comply controller, the full surface always loads.

In addition, importing any one module under adcp.types.domains.<domain> executes that domain's __init__, which imports all of its modules. As a result, one import loads about 911 of 1,088 domain modules, a floor of about 137 MiB.

Possible directions

  1. Let adcp.client, adcp.server.test_controller, adcp.decisioning and adcp.compat import only the models they reference, rather than adcp.types._eager.
  2. Make domain __init__ modules lazy with module-level __getattr__, as adcp.types already is, so importing one domain module doesn't load the whole domain.
  3. Add a CI check on import RSS (for example, import adcp.client under a fixed budget), so growth is visible in the PR that causes it.

Reproduce

for v in 6.6.0 9.3.0 10.0.0b1; do
  uv run -q --no-project --python 3.12 --with "adcp==$v" python -I -c \
    "import resource, adcp.client; print('$v', resource.getrusage(resource.RUSAGE_SELF).ru_maxrss)"
done   # ru_maxrss is in KiB on Linux, bytes on macOS

Activity

  1. added 3 commits that reference this issue on Oct 29, 2026
    a980761
    79ab8ca
    f9b9b03
  2. added 2 commits that reference this issue on Oct 29, 2026
    aac52ca
    ab39d63
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions