# Monty Resource Limits: How the Python Sandbox Enforces Security Boundaries

> Monty enforces resource limits to secure your Python sandbox. Discover how it caps heap allocations, execution time, memory, and recursion depth.

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

---

**Monty enforces resource limits through the `ResourceTracker` trait defined in [`crates/monty/src/resource.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/resource.rs), capping heap allocations, execution duration, memory consumption, and recursion depth to prevent untrusted Python code from exhausting host resources.**

Monty is a secure Python sandbox developed by Pydantic that executes untrusted code while preventing resource exhaustion attacks. Understanding Monty resource limits is essential for configuring appropriate security boundaries when running arbitrary Python code in production environments.

## Core Resource Limits Defined in Monty

Monty defines resource constraints through the `ResourceLimits` struct in [`crates/monty/src/resource.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/resource.rs). These limits are enforced by the `ResourceTracker` trait, which provides hooks for the interpreter to validate resource consumption at allocation sites, statement boundaries, and recursion points.

### Maximum Heap Allocations

The `ResourceLimits::max_allocations` field (line 73) defines the total number of heap allocations permitted during execution. The `LimitedTracker::on_allocate` method checks `self.allocation_count` against this limit on every allocation. If the count exceeds the configured maximum, the tracker returns `ResourceError::Allocation`, which Monty converts into a Python `MemoryError`.

### Execution Time Limits

Time-based resource limits are controlled by `ResourceLimits::max_duration` (line 76). The `LimitedTracker::check_time` method samples `Instant::elapsed()` every 10 VM steps (defined by `TIME_CHECK_INTERVAL`). When elapsed time exceeds `max_duration`, the tracker raises `ResourceError::Time`, which becomes a Python `TimeoutError`.

### Memory Usage Caps

The `ResourceLimits::max_memory` field (line 78) restricts total heap memory consumption in bytes. Both `LimitedTracker::on_allocate` and `LimitedTracker::check_large_result` verify that new memory usage stays below this threshold. If an allocation would exceed the limit, `ResourceError::Memory` is returned and converted to `MemoryError`.

### Recursion Depth Restrictions

Monty enforces two distinct recursion limits:

- **Call Stack Depth**: `ResourceLimits::max_recursion_depth` (line 82) controls Python function call recursion. `LimitedTracker::check_recursion_depth` validates the depth before pushing a new frame, returning `ResourceError::Recursion` (converted to `RecursionError`) if exceeded.

- **Data Structure Recursion**: `MAX_DATA_RECURSION_DEPTH` (lines 99‑115) limits recursion during data structure operations like `repr()` or equality checks. The `DepthGuard` struct (lines 121‑172) manages this counter, ensuring that operations on deeply nested containers raise a `RecursionError` before causing a native stack overflow.

### Large Result Pre-checks

Before performing operations that could allocate large contiguous blocks (such as huge exponentiation or string repetition), Monty calls `ResourceTracker::check_large_result`. The threshold `LARGE_RESULT_THRESHOLD` (line 18) is set to 100 KB. While the default `NoLimitTracker` permits any size, `LimitedTracker` verifies the requested size against `max_memory` and rejects it with `ResourceError::Memory` if it would exceed the cap.

## Runtime Enforcement: LimitedTracker vs. NoLimitTracker

Monty provides two implementations of the `ResourceTracker` trait:

- **`LimitedTracker`**: Enforces all configured `ResourceLimits` including allocations, time, memory, and recursion. Used when running untrusted code.
- **`NoLimitTracker`** (lines 22‑64): A permissive tracker that only enforces the default recursion limit of 1000 (matching CPython behavior). All other resource limits are disabled unless a custom `LimitedTracker` is supplied.

The `ResourceError` enum (lines 85‑94) defines concrete error variants (`Allocation`, `Time`, `Memory`, `Recursion`) that are turned into Python exceptions via `into_exception` (lines 122‑156).

## Configuring Resource Limits in Python

The `pydantic_monty` package exposes resource limits through the `ResourceLimits` class defined in [`crates/monty-python/src/limits.rs`](https://github.com/pydantic/monty/blob/main/crates/monty-python/src/limits.rs). You can configure limits using a fluent API:

```python
from pydantic_monty import Monty, ResourceLimits
import datetime

# Limit execution to 200 milliseconds

limits = ResourceLimits().max_duration(datetime.timedelta(milliseconds=200))
m = Monty("while True: pass", limits=limits)

try:
    m.run()
except TimeoutError as e:
    print("Timeout triggered:", e)

```

Memory and allocation limits work similarly:

```python
from pydantic_monty import Monty, ResourceLimits

# Allow at most 10,000 heap allocations and 5 MiB memory

limits = ResourceLimits().max_allocations(10_000).max_memory(5 * 1024 * 1024)
m = Monty("a = [0] * 10_000_000", limits=limits)

try:
    m.run()
except MemoryError as e:
    print("Memory limit hit:", e)

```

## Configuring Resource Limits in Rust

For internal use or custom integrations, you can construct a `LimitedTracker` directly in Rust:

```rust
use monty::{LimitedTracker, ResourceLimits};
use std::time::Duration;

let limits = ResourceLimits::new()
    .max_allocations(5_000)
    .max_memory(2 * 1024 * 1024)            // 2 MiB
    .max_duration(Duration::from_secs(1))
    .max_recursion_depth(Some(200));

let tracker = LimitedTracker::new(limits);
// Pass `tracker` into the VM execution context

```

The execution loop in [`crates/monty/src/run.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/run.rs) calls `check_time`, `check_recursion_depth`, and other tracker hooks at statement boundaries to ensure continuous enforcement.

## Summary

Monty implements a comprehensive resource limiting system to safely execute untrusted Python code:

- **Allocation caps** prevent excessive object creation via `max_allocations` enforced in `on_allocate`.
- **Time limits** interrupt infinite loops using `max_duration` checked every 10 VM steps.
- **Memory boundaries** restrict heap usage through `max_memory` verified during allocations and large-result operations.
- **Recursion guards** protect both the Python call stack (`max_recursion_depth`) and data structure operations (`MAX_DATA_RECURSION_DEPTH` via `DepthGuard`).
- **Large-result pre-checks** block operations that would allocate >100 KB contiguous blocks if they exceed memory limits.

These limits are configurable through the `ResourceLimits` struct and enforced by the `LimitedTracker` implementation, while `NoLimitTracker` provides a permissive mode for trusted code.

## Frequently Asked Questions

### What happens when a Monty resource limit is exceeded?

When a limit is exceeded, the `LimitedTracker` returns a `ResourceError` variant (such as `ResourceError::Allocation`, `ResourceError::Time`, or `ResourceError::Memory`). These errors are converted into Python exceptions via the `into_exception` method in [`crates/monty/src/resource.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/resource.rs). For example, exceeding the time limit raises `TimeoutError`, while exceeding memory or allocation limits raises `MemoryError`.

### Can I disable specific resource limits while keeping others active?

Yes. Monty provides the `NoLimitTracker` which disables all limits except the default recursion depth of 1000. To selectively disable limits, you can instantiate `ResourceLimits` with `None` for specific fields (where applicable) or use `NoLimitTracker` for unrestricted execution. For fine-grained control, implement a custom `ResourceTracker` trait that selectively enforces only the limits you require.

### How does Monty prevent stack overflow from deeply nested data structures?

Monty uses a separate `MAX_DATA_RECURSION_DEPTH` constant (defined at lines 99‑115 in [`crates/monty/src/resource.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/resource.rs)) to limit recursion during data structure operations like `repr()` or equality checks. The `DepthGuard` struct (lines 121‑172) manages this counter, ensuring that operations on deeply nested containers raise a `RecursionError` before causing a native stack overflow. This complements the call-stack limit enforced by `max_recursion_depth`.

### What is the performance impact of Monty's resource tracking?

The performance impact is minimal due to strategic sampling. For example, time checks occur only every 10 VM steps (`TIME_CHECK_INTERVAL`) rather than at every instruction. Allocation counters increment on every heap allocation but use simple integer arithmetic. The `NoLimitTracker` provides zero-overhead execution for trusted code by disabling all optional checks. According to the implementation in [`crates/monty/src/run.rs`](https://github.com/pydantic/monty/blob/main/crates/monty/src/run.rs), the tracker hooks are called at statement boundaries and allocation sites, ensuring safety without excessive overhead.