title: Investigate tokio-related memory leak in Connect transport status: scrapped type: bug priority: high created_at: 2026-07-09T15:34:48Z
Reported: memory usage grows over time when running chattolib against a live Chatto instance. Suspect the Rust runtime underneath connectrpc (pyqwest wraps reqwest which runs on tokio). Root-cause and mitigate — either through client-lifecycle changes in ChattoClient, an upstream connectrpc/pyqwest bug report, or a workaround.
Setup. Used psutil (RSS) and tracemalloc against a live Chatto HQ instance. Warmed up before measurements to skip cold-start allocations. gc.collect() before each reading.
Sustained RPC load on one ChattoClient — the scenario that first looked like a leak:
| RPCs | RSS |
|---|---|
| 500 | 78.8 MiB |
| 1000 | 82.9 MiB |
| 2000 | 88.1 MiB |
| 3000 | 90.0 MiB |
| 3500 | 84.2 MiB (dropped) |
| 5000 | 87.9 MiB |
| 7000 | 86.8 MiB (dropped) |
| 10000 | 87.2 MiB |
RSS plateaus around 85–91 MiB after ~2500 RPCs and drops back down at times. Not unbounded growth — it's the steady-state footprint of the tokio HTTP/2 connection pool + stream state.
tracemalloc across 500 RPCs after warm-up:
google.protobuf.json_format (MessageToDict)chattolib.types (dataclass parsers)asyncioSo ~93% of the growth is in the native (Rust/pyqwest/reqwest/tokio) layer, and the Python side is well-behaved.
I hypothesised the 24-services-per-client shape (each service instantiates its own pyqwest Client) was the culprit. Measured directly:
Reason: pyqwest.Client(transport=None) uses get_default_transport() — a process-wide shared tokio transport. The 24 Python-side Client objects are thin wrappers around the same native transport, so sharing is already happening under the hood.
30 seconds of live event streaming from Chatto HQ (27 events): RSS grew from 62.92 → 62.93 MiB. Fully stable.
No leak. What looked like a leak is the tokio connection pool reaching its steady-state footprint over the first ~2000-3000 request/response RPCs. Memory both grows and shrinks within that window and does not exceed ~90 MiB even at 10 000 RPCs.
If a production process is showing genuinely unbounded growth, the more likely culprits are:
Closing as not-a-bug. Followed up with detailed telemetry so future investigations start with data. If the user hits a real reproducer, reopen with the specific scenario and I'll instrument further.