gcmon - zero-overhead GC monitoring for Python
gcmon watches a running Python process's garbage collector from outside the process: no code changes, no callbacks, no in-process overhead. Export to Perfetto or JSONL; query with PerfettoSQL.
Requires CPython 3.15+ for the monitored process and the
gcmonprocess, built from the same source. See Limitations for details.
Why gcmon?
Python's garbage collector can introduce unpredictable pauses in applications.
The standard library provides gc.get_stats() for aggregate collection
counters and gc.callbacks for per-event hooks, but both run inside the
target process: callbacks add execution overhead that distorts timing, while
gc.get_stats() only exposes cumulative counters with no per-pause
resolution. Neither can monitor a process without modifying its code.
Most monitoring tools report the GC collection count, how often the collector ran. What hurts a latency-sensitive service is GC pause time, how long each collection held it up, and reporting that requires a source inside CPython's own GC bookkeeping. See Alternatives Comparison for what each tool reports.
gcmon reads GC statistics directly from a target process's memory using platform-specific memory access APIs. The target process is never paused (GC statistics are written to a ring buffer and read as a whole), so there is zero in-process overhead and no code changes required.
Use it to profile GC pause times, compare live-object count and RSS trends, or integrate GC metrics into benchmarks.
Features
- Real-time GC monitoring - Track garbage collection events in running Python processes without in-process overhead
- Multiple export formats - Perfetto binary protobuf, JSONL file, and JSONL to stdout (formats)
- CLI - Monitor processes or run scripts with GC monitoring (usage)
- RSS tracking - Track Resident Set Size of monitored processes in a Perfetto trace (details)
- Pyperf hook integration - Seamlessly integrate with pyperf benchmarks (pyperf hook)
When to Use
Use gcmon when you want to:
- Profile GC pause times in production or staging without modifying application code
- Measure GC impact on latency-sensitive services (APIs, real-time systems)
- Correlate GC activity with benchmark results via the pyperf hook
- Track live object count trends over time across running processes
- Debug intermittent latency spikes suspected to be GC-related
Use something else when you need to:
- Find which code paths trigger collections: use
profiling.sampling, oraustinwith-gon interpreters older than 3.15 (statistical, no per-pause timing or heap data) - In-process GC callbacks (e.g., triggering actions on collection): use
gc.callbacks - Cumulative collection counters without per-pause detail: use
gc.get_stats() - Monitor across different Python builds: gcmon requires the exact same binary (see Limitations)
Alternatives Comparison
| Tool | GC Pause Time | Code Changes | Overhead | Best Use Case |
|---|---|---|---|---|
| gcmon¹ | Yes (exact) | None | Zero in-process | Production GC monitoring |
profiling.sampling², austin |
Partial³ | None | Near-zero in-process | Which code triggers GC |
gc.callbacks |
Yes (exact) | High (custom code) | Moderate (Python call) | Custom metrics pipelines |
gc.get_stats() |
No (cumulative only) | Minimal | Minimal | Basic counters |
| APM agents (Datadog, New Relic, Dynatrace) | Varies⁴ | Agent required | Moderate | Distributed tracing |
| OpenTelemetry runtime metrics | No (counts only⁵) | Wrapper or SDK | Low | Fleet-wide GC counters |
¹ Requires CPython 3.15+ on both sides, built from the same source. See Limitations.
² Stdlib from CPython 3.15 on; austin covers older interpreters.
³ Both mark the samples taken during a collection (<GC> frames, austin's
-g), which gives GC as a share of samples and the stacks behind it, but no
per-pause durations and no heap data.
⁴ Datadog and New Relic ship theirs off by default: Datadog reports
per-generation collection counts, New Relic per-generation pause time via
gc.callbacks. Dynatrace collects GC activity per generation.
⁵ Reports collection counts (cpython.gc.collections and friends), not
durations. Platforms that bundle OTel, Odigos among them, forward the same
counters. eBPF sensors such as Groundcover's watch kernel events, not
CPython's GC phases.
Exact GC pause time has only two sources:
gc.callbacksinside the process, whether your own or an agent's, and_remote_debuggingreading CPython's ring buffer from outside it. Everything else samples or counts. gcmon builds on the latter.
Decision Guide
GC pauses: My service stalls and I suspect the collector. → Run gcmon against the PID for exact per-pause timings.
GC origin: I know collections are costly, but not what triggers them. →
Sample the process with
profiling.sampling
and read its <GC> frames.
How It Works
gcmon runs outside the target process. It reads GC statistics directly from the process's memory using platform-specific memory access APIs (available in CPython 3.15+). How gcmon reads a process covers the polling loop, what it misses, and what it recovers.
For the pyperf hook integration, gcmon uses an external process model:
- You start the monitor yourself, over the whole suite:
gcmon run - That process reads the benchmark process's memory directly
- The hook marks where each benchmark ran, in the trace the monitor writes
This provides zero in-process overhead during benchmarks, crash isolation (gcmon crashes don't affect the target), and clean separation of concerns.
Limitations
Same Python version and build
The monitoring and monitored processes must use the exact same Python
version and build. gcmon reads GC statistics directly from the target
process's in-memory data structures, and the layout of these structures varies
between Python versions and build configurations (fields, offsets, sizes).
Mismatched binaries are rejected by the Python runtime to prevent undefined
behavior or crashes.
In practice, run both processes from the same virtualenv, container image, or
pyenv/uv environment so they share a single Python binary.
Sub-step breakdown requires a custom build
The per-phase GC breakdown visible in the screenshot below (Mark Alive, Fill increment, Deduce Unreachable, and the fields that accompany it) is only produced when the monitored process runs a CPython build with enhanced GC instrumentation. Standard CPython builds give you the top-level GC Pause slices and counter data only. See Output formats for which fields need which build.
Not every GC run is read
CPython writes one record per finished GC run into a small fixed ring buffer, so a target whose collector runs more often than gcmon polls loses records before any poll reads them. A GC-heavy workload at default settings can sit there for most of a run.
gcmon reconstructs what it missed from CPython's cumulative counters, so
Count and Sum in the --stats table cover every run, read or not, and
the Cov column reports what share gcmon read. Percentiles are not
corrected and read high. The trace draws each blind interval on a GC Loss
track. See
How gcmon reads a process
for the mechanism and
Statistics
for how to read a low-coverage table.
No call-stack attribution
gcmon reports when each GC run happened, how long it took, and how large the heap was, plus a per-phase breakdown on a custom CPython build with enhanced GC instrumentation (see above). It cannot tell you which code triggered the run, because the GC records carry no stack information. A sampler answers that question, so the two pair well: see Alternatives Comparison.
No OS-level memory pressure
gcmon reports the collector's view of the heap, plus RSS samples when --rss
is enabled. Neither is a measure of OS-level memory pressure. Use psutil,
Prometheus node exporters, or eBPF tooling for that.
Requirements
- Python: CPython 3.15 or newer is required for both the monitoring and the monitored process.
- Operating systems: Linux, macOS, and Windows are supported (the test
matrix runs on
ubuntu-latest,macos-latest, andwindows-latest). - Process access: gcmon reads another process's memory using platform-specific APIs. On Linux and Windows no extra setup is needed. On macOS, the calling process must be authorized to read the target process memory.
Installation
pip install gcmon
# With optional extras
pip install gcmon[stats] # High-accuracy statistics (see docs/statistics.md)
pip install gcmon[cmdline] # Process command line and RSS tracking (see docs/rss.md)
pip install gcmon[stats,cmdline] # Both extras
[stats] installs DDSketch for
high-accuracy, memory-efficient percentiles; see
Statistics.
[cmdline] installs psutil, which
populates process command lines in Perfetto traces and enables --rss; see
RSS Tracking.
Each extra degrades gracefully when absent; no other trace data is affected.
Quick Start
# Monitor a running process by PID (default Perfetto format)
gcmon 12345
# Run a Python script with GC monitoring
gcmon run -s my_script.py
# Monitor with custom output and statistics output
gcmon monitor 12345 -o trace.pftrace --stats=total
# Perfetto binary output with RSS tracking
gcmon 12345 --format perfetto -o trace.pftrace --rss
# Combine multiple JSONL captures (e.g. different runs or builds) into one trace
gcmon combine trace1.jsonl trace2.jsonl -o combined.pftrace -n
Example: Perfetto Trace Output
GC monitoring data visualized in Perfetto UI:
- GC Pause slices with sub-step breakdown, and per-gen
G{gen}counter tracks - A shared
heap_sizecounter and aProcesseslifetime track - An
rsscounter track per PID (when--rssis enabled)
See Output formats for the full track inventory and the JSONL event schema.
See Also
The tools weighed in Alternatives Comparison, and the viewer gcmon writes for:
profiling.sampling: stdlib statistical profiler, Tachyon (out-of-process,<GC>frames but no per-pause timing)austin: sampling CPU/memory profiler (out-of-process,-gtags GC samples on interpreters older than 3.15)gc.callbacks: in-process hook, the other exact source of pause timegc.get_stats(): cumulative per-generation counters, no per-pause detail- OpenTelemetry runtime metrics: fleet-wide GC collection counts
- Perfetto UI: the trace viewer used by gcmon's Perfetto exporter
Project documentation
- gcmon documentation: CLI reference, output formats, statistics, RSS tracking, the pyperf hook, programmatic control, Perfetto SQL, architecture decision records, and the release process
License
MIT License - see LICENSE for details.
Contributing
Bug reports and pull requests are welcome at GitHub. The contributing guide covers setting up a working copy, running the tests and the checkers, and what a pull request needs. Please read the AI policy first.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file gcmon-0.7.0.tar.gz.
File metadata
- Download URL: gcmon-0.7.0.tar.gz
- Upload date:
- Size: 81.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3f6c7b63f1205475ff8340a72e6d9d02c4d451f7c782fdfa4cac46f4f07bccd8
|
|
| MD5 |
8f0ebc09cbe0bb56c88eb5bdfa285004
|
|
| BLAKE2b-256 |
224aaa3ab1a3ef9387759ba2bc5a647458e7b54e1a9489930e1059040d3f395f
|
Provenance
The following attestation bundles were made for gcmon-0.7.0.tar.gz:
Publisher:
release.yml on sergey-miryanov/gcmon
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
gcmon-0.7.0.tar.gz -
Subject digest:
3f6c7b63f1205475ff8340a72e6d9d02c4d451f7c782fdfa4cac46f4f07bccd8 - Sigstore transparency entry: 2767830556
- Sigstore integration time:
-
Permalink:
sergey-miryanov/gcmon@755b03e5366e51df1ea790a06d975a6c69d24a0d -
Branch / Tag:
refs/tags/v0.7.0 - Owner: https://github.com/sergey-miryanov
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@755b03e5366e51df1ea790a06d975a6c69d24a0d -
Trigger Event:
push
-
Statement type:
File details
Details for the file gcmon-0.7.0-py3-none-any.whl.
File metadata
- Download URL: gcmon-0.7.0-py3-none-any.whl
- Upload date:
- Size: 101.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
0a7ecccc37393e06eff3951351d3b20947cc1c3d8ab70fc0499a86c4b968e0cf
|
|
| MD5 |
8b3cb4894894048540feb6c50423e989
|
|
| BLAKE2b-256 |
cb12712853913c38c1a435727bf78a915f909a9d8218f290d0bf58018df311ac
|
Provenance
The following attestation bundles were made for gcmon-0.7.0-py3-none-any.whl:
Publisher:
release.yml on sergey-miryanov/gcmon
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
gcmon-0.7.0-py3-none-any.whl -
Subject digest:
0a7ecccc37393e06eff3951351d3b20947cc1c3d8ab70fc0499a86c4b968e0cf - Sigstore transparency entry: 2767830569
- Sigstore integration time:
-
Permalink:
sergey-miryanov/gcmon@755b03e5366e51df1ea790a06d975a6c69d24a0d -
Branch / Tag:
refs/tags/v0.7.0 - Owner: https://github.com/sergey-miryanov
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@755b03e5366e51df1ea790a06d975a6c69d24a0d -
Trigger Event:
push
-
Statement type: