# How Monty Tracks Memory Allocations in Python Objects

> Discover how Monty tracks memory allocations in Python using a cooperative Heap arena and pluggable ResourceTracker to record statistics and enforce limits.

- Repository: [Pydantic/monty](https://github.com/pydantic/monty)
- Tags: internals
- Published: 2026-02-16

---

**Monty tracks memory allocations through a cooperative system where the `Heap` arena forwards object sizes to a pluggable `ResourceTracker` trait that records statistics and enforces limits before any allocation succeeds.**

Monty is a secure Python interpreter built by Pydantic designed to execute untrusted code safely. Understanding how Monty tracks memory allocations is essential for configuring resource limits and debugging memory usage in sandboxed environments. The system relies on precise accounting at the moment of allocation and deallocation to prevent memory exhaustion attacks.

## The Core Architecture: Heap and ResourceTracker

Monty’s memory tracking operates through two primary components defined in [`crates/monty/src/heap.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/heap.rs) and [`crates/monty/src/resource.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/resource.rs):

- **`Heap<T>`**: The arena that stores every heap-allocated Python object. It manages object slots, reference counting, and garbage collection flags.
- **`ResourceTracker`**: A trait that defines hooks for allocation events, allowing pluggable policies for statistics gathering and limit enforcement.

When the VM creates a new object, it calls `Heap::allocate`, which immediately consults the attached tracker before committing memory.

## How Allocation Tracking Works

### The Allocation Flow in `Heap::allocate`

The `allocate` method in [`crates/monty/src/heap.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/heap.rs) serves as the central checkpoint for all heap objects. Before storing any value, it invokes `on_allocate` to verify that the operation complies with resource limits:

```rust
pub fn allocate(&mut self, data: HeapData) -> Result<HeapId, ResourceError> {
    // 1. Ask the tracker how many bytes this allocation will need.
    self.tracker.on_allocate(|| data.py_estimate_size())?;

    // 2. For GC‑tracked containers, bump the allocation counter and
    //    mark that a reference cycle may exist.
    if data.is_gc_tracked() {
        self.allocations_since_gc = self.allocations_since_gc.wrapping_add(1);
        if data.has_refs() {
            self.may_have_cycles = true;
        }
    }

    // 3. Store the value (or reuse a freed slot) and return its `HeapId`.
    // ... implementation details ...
}

```

The closure passed to `on_allocate` calculates the object size lazily, ensuring the tracker only computes the value when necessary.

### Counting Allocations with `LimitedTracker`

The default concrete implementation, `LimitedTracker` in [`crates/monty/src/resource.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/resource.rs), maintains two critical counters: `allocation_count` and `current_memory`. Its `on_allocate` method validates limits before incrementing these counters:

```rust
fn on_allocate(&mut self, get_size: impl FnOnce() -> usize) -> Result<(), ResourceError> {
    if let Some(max) = self.limits.max_allocations && self.allocation_count >= max {
        return Err(ResourceError::Allocation { limit: max, count: self.allocation_count + 1 });
    }

    let size = get_size();
    if let Some(max) = self.limits.max_memory {
        let new_memory = self.current_memory + size;
        if new_memory > max {
            return Err(ResourceError::Memory { limit: max, used: new_memory });
        }
    }

    self.allocation_count += 1;
    self.current_memory += size;
    Ok(())
}

```

If either limit is exceeded, the method returns a `ResourceError` immediately, causing `Heap::allocate` to fail before any memory is committed.

## Deallocation and Memory Accounting

Monty tracks deallocations through reference counting. When an object's reference count drops to zero, `Heap::dec_ref` in [`crates/monty/src/heap.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/heap.rs) reclaims the slot and notifies the tracker:

```rust
if entry.refcount > 1 {
    entry.refcount -= 1;
} else if let Some(value) = slot.take() {
    self.free_list.push(id);
    if let Some(ref data) = value.data {
        self.tracker.on_free(|| data.py_estimate_size());
    }
    // Recursively dec‑ref child objects …
}

```

The `on_free` hook allows `LimitedTracker` to decrement `current_memory` and maintain accurate statistics. Notably, `allocation_count` is not decremented on free; it represents the cumulative number of allocations made during execution, not the current live count.

## Garbage Collection Integration

Monty uses a generational garbage collector to handle reference cycles. The `Heap` tracks GC-related metadata during allocation:

- **`allocations_since_gc`**: A counter incremented whenever a GC-tracked container (like `List` or `Dict`) is allocated. When this exceeds `gc_interval`, the VM triggers a collection cycle.
- **`may_have_cycles`**: A boolean flag set when a newly allocated container contains references to other objects. This signals the GC that a cycle might exist.

These mechanisms ensure that memory tracking remains accurate even when objects form cycles that prevent immediate deallocation through reference counting alone.

## Enforcing Resource Limits

Beyond simple allocation counting, the `ResourceTracker` trait provides hooks for additional safety checks:

- **`check_time`**: Called periodically during VM execution to enforce timeout limits.
- **`check_recursion_depth`**: Validates that the call stack has not exceeded a configured maximum depth.
- **`check_large_result`**: Preemptively denies operations that would produce results larger than 100 KB, preventing memory bombs from large string or list concatenations.

These checks are lightweight and strategically placed at interpreter entry points to minimize performance overhead while maintaining strict resource boundaries.

## Practical Example

The following example demonstrates how to configure a `Heap` with strict allocation and memory limits using `LimitedTracker`:

```rust
use monty::resource::{LimitedTracker, ResourceLimits};
use monty::heap::Heap;
use monty::value::Value;

// 1️⃣ Create a tracker that caps allocations at 10 000 objects and memory at 2 MiB.
let limits = ResourceLimits::default()
    .max_allocations(10_000)
    .max_memory(2 * 1024 * 1024);
let tracker = LimitedTracker::new(limits);

// 2️⃣ Build a heap that uses this tracker.
let mut heap = Heap::new(0, tracker);

// 3️⃣ Allocate a Python string – the tracker records the allocation.
let hello_id = heap.allocate(monty::types::Str::new("hello".into()))?;
assert_eq!(heap.tracker().allocation_count(), 1);

// 4️⃣ Release the string – the tracker’s `on_free` updates memory usage.
heap.dec_ref(hello_id);
assert_eq!(heap.tracker().allocation_count(), 1); // count stays, memory drops

// 5️⃣ Trigger a limit violation.
let huge = "x".repeat(5_000_000); // > 5 MiB
match heap.allocate(monty::types::Str::new(huge.into())) {
    Err(e) => println!("allocation failed: {}", e), // prints MemoryError
    Ok(_) => {}
}

```

This example illustrates how Monty tracks memory allocations at the granularity of individual Python objects, enabling precise resource control and safe execution of untrusted code.

## Summary

- Monty tracks memory allocations through a cooperation between the `Heap` arena in [`crates/monty/src/heap.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/heap.rs) and the pluggable `ResourceTracker` trait in [`crates/monty/src/resource.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/resource.rs).
- Every allocation passes through `Heap::allocate`, which calls `tracker.on_allocate` to verify limits before committing memory.
- The default `LimitedTracker` maintains `allocation_count` and `current_memory` counters, enforcing `max_allocations` and `max_memory` limits.
- Deallocations are tracked via `Heap::dec_ref` calling `tracker.on_free`, ensuring `current_memory` remains accurate as objects are freed.
- GC-tracked containers increment `allocations_since_gc` and set `may_have_cycles` to coordinate garbage collection with memory tracking.

## Frequently Asked Questions

### How does Monty prevent memory exhaustion from untrusted code?

Monty prevents memory exhaustion by checking `max_memory` and `max_allocations` limits inside `LimitedTracker::on_allocate` before any heap memory is committed. If the new allocation would exceed the configured threshold, the method returns `ResourceError::Memory` or `ResourceError::Allocation`, causing the VM to raise a `MemoryError` in the Python environment without actually consuming the resources.

### What is the difference between `allocation_count` and `current_memory` in Monty?

`allocation_count` tracks the cumulative number of successful allocation operations since the heap was created, while `current_memory` tracks the total bytes currently allocated on the heap. When an object is freed via `dec_ref`, `current_memory` decreases by the object's size, but `allocation_count` remains unchanged because it represents a historical statistic rather than a live counter.

### How does Monty track memory for Python objects that contain references to other objects?

When allocating GC-tracked containers like `List` or `Dict`, `Heap::allocate` checks `data.is_gc_tracked()` and increments `allocations_since_gc`. If the container contains references (`data.has_refs()`), it sets `may_have_cycles = true`. While the memory tracking for the container itself is handled via `py_estimate_size()` during allocation, the reference tracking ensures the garbage collector can break cycles when objects become unreachable, allowing `dec_ref` to eventually reclaim the memory and call `on_free`.

### Can Monty enforce limits on execution time and recursion depth as well as memory?

Yes, the `ResourceTracker` trait provides additional hooks beyond memory tracking. `check_time` enforces timeout limits by returning an error if the execution time exceeds a threshold, while `check_recursion_depth` validates that the call stack has not exceeded `max_recursion_depth`. These checks are invoked at strategic points in the interpreter, such as before function calls or during VM instruction loops, ensuring comprehensive resource control alongside memory allocation tracking.