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
Type Memory Usage Integers Variable (based on value size) Floats 8 bytes Strings 1 byte per character + overhead Lists ~8 bytes per element + overhead Dicts ~24 bytes per entry + overhead Tuples ~8 bytes per element + overhead Objects Sum 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.
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 Type Python Exception Memory MemoryErrorAllocations MemoryErrorTime TimeoutErrorRecursion RecursionError
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.
Recommended Limits
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
Always Set Limits for Untrusted Code
Never run untrusted code without resource limits. Always configure at least memory and time limits.
Set Conservative Limits Initially
Start with strict limits and increase based on actual usage patterns.
Monitor and Alert
Log resource limit violations to detect potential attacks or bugs.
Adjust Time Limits on Resume
When using iterative execution, set appropriate time limits before each resume().
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