Skip to main content

Overview

Monty enforces configurable resource limits to prevent untrusted code from consuming excessive resources. Without limits, malicious code could:
  • Allocate gigabytes of memory
  • Run infinite loops
  • Cause stack overflow with deep recursion
  • Generate massive strings or lists
Resource limits ensure safe execution of untrusted code.

Available Limits

Monty tracks four types of resource limits:

Max Memory

Limit total heap memory usage in bytes

Max Duration

Timeout after specified execution time

Max Allocations

Limit number of heap allocations

Max Recursion Depth

Prevent stack overflow (default: 1000)

Setting Resource Limits

import pydantic_monty
from datetime import timedelta

# Configure limits
limits = pydantic_monty.ResourceLimits(
    max_memory=1_000_000,        # 1 MB
    max_duration=timedelta(seconds=5),
    max_allocations=10_000,
    max_recursion_depth=100
)

m = pydantic_monty.Monty('x * 2', inputs=['x'])

try:
    result = m.run(inputs={'x': 42}, limits=limits)
    print(result)
except pydantic_monty.MontyException as e:
    print(f"Resource limit exceeded: {e}")

Memory Limit

How It Works

Monty tracks approximate heap memory usage and checks before each allocation:
limits = pydantic_monty.ResourceLimits(max_memory=100_000)  # 100 KB

# This will raise MemoryError
code = "'x' * 1_000_000"  # Try to create 1 MB string
m = pydantic_monty.Monty(code)
try:
    m.run(limits=limits)
except pydantic_monty.MontyException as e:
    print(e.exc_type)  # MemoryError
    print(e.message)   # memory limit exceeded: 1000000 bytes > 100000 bytes

Large Result Pre-checks

Monty pre-checks operations that may produce large results (>100KB) before allocating:
# These are checked BEFORE execution
code = """
# String repeat
result = 'x' * 10_000_000

# Power (large integers)
result = 2 ** 10_000_000

# List repeat
result = [1, 2, 3] * 1_000_000

# String replace (amplification)
result = ('a' * 1000).replace('a', 'b' * 10_000)
"""
The 100KB threshold (LARGE_RESULT_THRESHOLD) is compile-time and cannot be changed at runtime.

What Counts Toward Memory

TypeMemory Usage
IntegersVariable (based on value size)
Floats8 bytes
Strings1 byte per character + overhead
Lists~8 bytes per element + overhead
Dicts~24 bytes per entry + overhead
Tuples~8 bytes per element + overhead
ObjectsSum of field sizes + overhead

Time Limit

How It Works

Monty checks elapsed time periodically during execution:
from datetime import timedelta

limits = pydantic_monty.ResourceLimits(
    max_duration=timedelta(seconds=1)
)

# Infinite loop will timeout
code = """
while True:
    x = 1 + 1
"""

m = pydantic_monty.Monty(code)
try:
    m.run(limits=limits)
except pydantic_monty.MontyException as e:
    print(e.exc_type)  # TimeoutError
    print(e.message)   # time limit exceeded: 1.002s > 1s

Time Check Frequency

Monty checks time every 10 VM instructions to balance:
  • Performance (checking every instruction is expensive)
  • Responsiveness (catching timeouts quickly)
Time is reset after load() when deserializing execution state. Use tracker_mut() to set a new limit when resuming.

Setting Time Limits on Resume

import pydantic_monty
from datetime import timedelta

progress = m.start(inputs={'x': 42})

if isinstance(progress, pydantic_monty.FunctionSnapshot):
    # Modify time limit before resuming
    progress.tracker_mut().set_max_duration(
        timedelta(seconds=2)
    )
    
    result = progress.resume(return_value=100)

Allocation Limit

How It Works

Counts the total number of heap allocations:
limits = pydantic_monty.ResourceLimits(max_allocations=1000)

# Each list/string/dict allocation counts
code = """
result = []
for i in range(2000):  # Will exceed 1000 allocations
    result.append(f"item {i}")
"""

m = pydantic_monty.Monty(code)
try:
    m.run(limits=limits)
except pydantic_monty.MontyException as e:
    print(e.exc_type)  # MemoryError
    print(e.message)   # allocation limit exceeded: 1001 > 1000

When to Use Allocation Limits

Use allocation limits to:
  • Prevent memory fragmentation
  • Control garbage collection frequency
  • Limit total number of objects
Allocation limits are useful when combined with garbage collection intervals to control GC overhead.

Recursion Depth Limit

How It Works

Limits the maximum call stack depth:
limits = pydantic_monty.ResourceLimits(max_recursion_depth=50)

code = """
def recursive(n):
    if n == 0:
        return 0
    return recursive(n - 1)

recursive(100)  # Will exceed depth of 50
"""

m = pydantic_monty.Monty(code)
try:
    m.run(limits=limits)
except pydantic_monty.MontyException as e:
    print(e.exc_type)  # RecursionError
    print(e.message)   # maximum recursion depth exceeded

Default Recursion Limit

If not specified, Monty uses 1000 (same as CPython’s default).
RecursionError is catchable in Python, unlike other resource errors. Untrusted code can catch and suppress RecursionError.

Platform Considerations

Very deep recursion (>500) may cause stack overflow in debug builds. Release builds handle 1000+ safely.

No Limits Mode

For trusted code, use NoLimitTracker to disable limits:
import pydantic_monty

# No limits - only default recursion depth (1000)
m = pydantic_monty.Monty('x ** 100000', inputs=['x'])
result = m.run(inputs={'x': 2})  # No limits by default
Never use NoLimitTracker with untrusted code. It only enforces recursion depth (1000).

Exception Types

Resource limit violations raise specific Python exceptions:
Limit TypePython Exception
MemoryMemoryError
AllocationsMemoryError
TimeTimeoutError
RecursionRecursionError

Handling Resource Errors

import pydantic_monty

try:
    result = m.run(inputs={'x': 42}, limits=limits)
except pydantic_monty.MontyException as e:
    if e.exc_type == 'MemoryError':
        print("Code used too much memory")
    elif e.exc_type == 'TimeoutError':
        print("Code took too long")
    elif e.exc_type == 'RecursionError':
        print("Code recursed too deeply")

Garbage Collection

GC Intervals

Configure how often garbage collection runs:
limits = pydantic_monty.ResourceLimits(
    gc_interval=1000  # Run GC every 1000 allocations
)

When to Adjust GC Interval

  • Lower interval (e.g., 100): More frequent GC, lower peak memory
  • Higher interval (e.g., 10000): Less GC overhead, higher peak memory
  • No interval (None): Only run GC on allocation failure
Garbage collection only matters for code that creates reference cycles (circular references). Most code doesn’t need GC.

For User-Generated Code

limits = pydantic_monty.ResourceLimits(
    max_memory=10_000_000,      # 10 MB
    max_duration=timedelta(seconds=10),
    max_allocations=100_000,
    max_recursion_depth=100,
    gc_interval=1000
)

For LLM-Generated Code

limits = pydantic_monty.ResourceLimits(
    max_memory=50_000_000,      # 50 MB
    max_duration=timedelta(seconds=30),
    max_allocations=500_000,
    max_recursion_depth=200,
    gc_interval=5000
)

For Batch Processing

limits = pydantic_monty.ResourceLimits(
    max_memory=100_000_000,     # 100 MB
    max_duration=timedelta(minutes=5),
    max_allocations=1_000_000,
    max_recursion_depth=500,
    gc_interval=10000
)

Monitoring Resource Usage

Track resource usage during execution:
import pydantic_monty

limits = pydantic_monty.ResourceLimits(
    max_memory=1_000_000,
    max_allocations=10_000
)

# In iterative execution, you can inspect the tracker
progress = m.start(inputs={'x': 42}, limits=limits)

if isinstance(progress, pydantic_monty.FunctionSnapshot):
    tracker = progress.tracker_mut()
    
    # Check current usage (Rust API)
    # print(f"Memory used: {tracker.current_memory()}")
    # print(f"Allocations: {tracker.allocation_count()}")
    # print(f"Elapsed: {tracker.elapsed()}")
    
    result = progress.resume(return_value=100)
Resource monitoring APIs are available in the Rust API. Python/TypeScript bindings may be added in future versions.

Best Practices

1

Always Set Limits for Untrusted Code

Never run untrusted code without resource limits. Always configure at least memory and time limits.
2

Set Conservative Limits Initially

Start with strict limits and increase based on actual usage patterns.
3

Monitor and Alert

Log resource limit violations to detect potential attacks or bugs.
4

Adjust Time Limits on Resume

When using iterative execution, set appropriate time limits before each resume().
5

Balance GC Frequency

Adjust gc_interval based on your memory vs. performance requirements.

Next Steps

Security Model

Learn about Monty’s sandbox isolation and security guarantees

Execution Modes

Understand run() vs start()/resume() execution

Build docs developers (and LLMs) love