Skill Tracer
Skills › Scientific Agent Skills › modal › scaling.md

scaling.md

199 lines · skills/modal/references/scaling.md at 5f2bc03

Open on GitHub

Most marks come from matching words in the file, so treat them as pointers.

1# Modal Scaling and Concurrency
2
3Reviewed against SDK 1.6.0 and current [scaling](https://modal.com/docs/guide/scale),
4[invocation](https://modal.com/docs/guide/function-invocation-methods),
5[concurrency](https://modal.com/docs/guide/concurrent-inputs),
6[batching](https://modal.com/docs/guide/dynamic-batching) and
7[dynamic configuration](https://modal.com/docs/guide/dynamic-function-config) guides.
8Workload placeholders below are illustrative.
9

Modal Scaling and Concurrency

Reviewed against SDK 1.6.0 and current scaling, invocation, concurrency, batching and dynamic configuration guides. Workload placeholders below are illustrative.

Raw lines 1 to 9
10## Table of Contents
12- [Autoscaling](#autoscaling)
13- [Configuration](#configuration)
14- [Parallel Execution](#parallel-execution)
15- [Concurrent Inputs](#concurrent-inputs)
16- [Dynamic Batching](#dynamic-batching)
17- [Dynamic Autoscaler Updates](#dynamic-autoscaler-updates)
18- [Limits](#limits)

Table of Contents

  • Autoscaling
  • Configuration
  • Parallel Execution
  • Concurrent Inputs
  • Dynamic Batching
  • Dynamic Autoscaler Updates
  • Limits
Raw lines 10 to 19
20## Autoscaling
22Modal automatically manages a pool of containers for each function:
23- Spins up containers when there's no capacity for new inputs
24- Spins down idle containers to save costs
25- Scales container compute to zero by default; persistent storage remains separately billed
27No configuration needed for basic autoscaling — it works out of the box.

Autoscaling

Modal automatically manages a pool of containers for each function:

  • Spins up containers when there's no capacity for new inputs
  • Spins down idle containers to save costs
  • Scales container compute to zero by default; persistent storage remains separately billed

No configuration needed for basic autoscaling — it works out of the box.

Raw lines 20 to 28
29## Configuration
31Fine-tune autoscaling behavior:
33```python
34@app.function(
35 max_containers=100, # Upper limit on container count
36 min_containers=2, # Keep 2 warm (reduces cold starts)
37 buffer_containers=5, # Reserve 5 extra for burst traffic
38 scaledown_window=300, # Wait 5 min idle before shutting down
40def handle_request(data):
41 ...
42```
44| Parameter | Default | Description |
45|-----------|---------|-------------|
46| `max_containers` | Platform/workspace limit | Hard cap for this Function pool |
47| `min_containers` | 0 | Minimum warm containers (costs money even when idle) |
48| `buffer_containers` | Server-selected when omitted | Extra idle containers while active |
49| `scaledown_window` | Server-selected when omitted | Maximum idle seconds while scaling down |
51SDK defaults are `None` for omitted settings, allowing platform defaults. Set values
52explicitly when required. Containers can shut down sooner than the scaledown window
53when overprovisioned. Warm pools reduce cold starts but cannot guarantee their absence.

Configuration

Fine-tune autoscaling behavior:

@app.function(
    max_containers=100,     # Upper limit on container count
    min_containers=2,       # Keep 2 warm (reduces cold starts)
    buffer_containers=5,    # Reserve 5 extra for burst traffic
    scaledown_window=300,   # Wait 5 min idle before shutting down
)
def handle_request(data):
    ...
ParameterDefaultDescription
max_containersPlatform/workspace limitHard cap for this Function pool
min_containers0Minimum warm containers (costs money even when idle)
buffer_containersServer-selected when omittedExtra idle containers while active
scaledown_windowServer-selected when omittedMaximum idle seconds while scaling down

SDK defaults are None for omitted settings, allowing platform defaults. Set values explicitly when required. Containers can shut down sooner than the scaledown window when overprovisioned. Warm pools reduce cold starts but cannot guarantee their absence.

Raw lines 29 to 54
55### Trade-offs
57- Higher `min_containers` = lower latency, higher cost
58- Higher `buffer_containers` = less queuing, higher cost
59- Lower `scaledown_window` = faster cost savings, more cold starts

Trade-offs

  • Higher min_containers = lower latency, higher cost
  • Higher buffer_containers = less queuing, higher cost
  • Lower scaledown_window = faster cost savings, more cold starts
Raw lines 55 to 60
61## Parallel Execution

Parallel Execution

Raw lines 61 to 62
63### `.map()` — Process Many Inputs
65```python
66@app.function()
67def process(item):
68 return heavy_computation(item)
70@app.local_entrypoint()
71def main():
72 items = list(range(10_000))
73 results = list(process.map(items))
74```
76Modal automatically scales containers to handle the workload. Results maintain input order.

.map() — Process Many Inputs

@app.function()
def process(item):
    return heavy_computation(item)

@app.local_entrypoint()
def main():
    items = list(range(10_000))
    results = list(process.map(items))

Modal automatically scales containers to handle the workload. Results maintain input order.

Raw lines 63 to 77
78### `.map()` Options
80```python
81# Unordered results (faster)
82for result in process.map(items, order_outputs=False):
83 handle(result)
85# Collect errors instead of raising
86results = list(process.map(items, return_exceptions=True))
87for r in results:
88 if isinstance(r, Exception):
89 print(f"Error: {r}")
90```

.map() Options

# Unordered results (faster)
for result in process.map(items, order_outputs=False):
    handle(result)

# Collect errors instead of raising
results = list(process.map(items, return_exceptions=True))
for r in results:
    if isinstance(r, Exception):
        print(f"Error: {r}")
Raw lines 78 to 91
92### `.starmap()` — Multi-Argument
94```python
95@app.function()
96def add(x, y):
97 return x + y
99results = list(add.starmap([(1, 2), (3, 4), (5, 6)]))
100# [3, 7, 11]
101```

.starmap() — Multi-Argument

@app.function()
def add(x, y):
    return x + y

results = list(add.starmap([(1, 2), (3, 4), (5, 6)]))
# [3, 7, 11]
Raw lines 92 to 102
103### `.spawn()` — Fire-and-Forget
105```python
106# Returns immediately
107call = process.spawn(large_data)
109# Check status or get result later
110result = call.get()
111```
113Up to 1 million queued `.spawn()` inputs. Use a deployed Function for durable jobs,
114persist the returned call ID, and retrieve outputs within 7 days of completion.
115`spawn_map()` submits many inputs but returns **no result handle** in SDK 1.6.0;
116each worker must persist its output if it matters.

.spawn() — Fire-and-Forget

# Returns immediately
call = process.spawn(large_data)

# Check status or get result later
result = call.get()

Up to 1 million queued .spawn() inputs. Use a deployed Function for durable jobs, persist the returned call ID, and retrieve outputs within 7 days of completion. spawn_map() submits many inputs but returns no result handle in SDK 1.6.0; each worker must persist its output if it matters.

Raw lines 103 to 117
Starts an agent spawn :103
118## Concurrent Inputs
120By default, each container handles one input at a time. Use `@modal.concurrent` to handle multiple:
122```python
123@app.function(gpu="L40S")
124@modal.concurrent(max_inputs=10)
125async def predict(text: str):
126 result = await model.predict_async(text)
127 return result
128```
130This is ideal for I/O-bound workloads or async inference where a single GPU can handle multiple requests.
131Synchronous functions use threads; async functions share an event loop. Avoid blocking
132that loop. `target_inputs` can be set below `max_inputs` to give autoscaling headroom.

Concurrent Inputs

By default, each container handles one input at a time. Use @modal.concurrent to handle multiple:

@app.function(gpu="L40S")
@modal.concurrent(max_inputs=10)
async def predict(text: str):
    result = await model.predict_async(text)
    return result

This is ideal for I/O-bound workloads or async inference where a single GPU can handle multiple requests. Synchronous functions use threads; async functions share an event loop. Avoid blocking that loop. target_inputs can be set below max_inputs to give autoscaling headroom.

Raw lines 118 to 133
134### With Web Endpoints
136```python
137@app.function(gpu="L40S")
138@modal.concurrent(max_inputs=20)
139@modal.asgi_app()
140def web_service():
141 return fastapi_app
142```

With Web Endpoints

@app.function(gpu="L40S")
@modal.concurrent(max_inputs=20)
@modal.asgi_app()
def web_service():
    return fastapi_app
Raw lines 134 to 143
144## Dynamic Batching
146Collect inputs into batches for efficient GPU utilization:
148```python
149@app.function(gpu="L40S")
150@modal.batched(max_batch_size=32, wait_ms=100)
151async def batch_predict(texts: list[str]):
152 # Called with up to 32 texts at once
153 embeddings = model.encode(texts)
154 return list(embeddings)
155```
157- `max_batch_size` — Maximum inputs per batch
158- `wait_ms` — How long to wait for more inputs before processing
159- The function receives a list and must return a list of the same length
160- Callers submit individual elements, e.g. `batch_predict.remote("text")`; the decorator batches them
161- A class with a batched method cannot contain other Modal methods; validate the selected configuration before enabling concurrency on a batched workload

Dynamic Batching

Collect inputs into batches for efficient GPU utilization:

@app.function(gpu="L40S")
@modal.batched(max_batch_size=32, wait_ms=100)
async def batch_predict(texts: list[str]):
    # Called with up to 32 texts at once
    embeddings = model.encode(texts)
    return list(embeddings)
  • max_batch_size — Maximum inputs per batch
  • wait_ms — How long to wait for more inputs before processing
  • The function receives a list and must return a list of the same length
  • Callers submit individual elements, e.g. batch_predict.remote("text"); the decorator batches them
  • A class with a batched method cannot contain other Modal methods; validate the selected configuration before enabling concurrency on a batched workload
Raw lines 144 to 162
163## Dynamic Autoscaler Updates
165Adjust autoscaling at runtime without redeploying:
167```python
168@app.function()
169def scale_up_for_peak():
170 process = modal.Function.from_name("my-app", "process")
171 process.update_autoscaler(min_containers=10, buffer_containers=20)
173@app.function()
174def scale_down_after_peak():
175 process = modal.Function.from_name("my-app", "process")
176 process.update_autoscaler(min_containers=1, buffer_containers=2)
177```
179Settings revert to the decorator values on the next deployment.
180`with_options()`, `with_concurrency()` and `with_batching()` instead create variants
181with separate container pools. A base `max_containers` does not cap the sum of variants;
182avoid high-cardinality configurations. Variants ignore base `min_containers`, and
183`with_options()` does not accept that parameter.

Dynamic Autoscaler Updates

Adjust autoscaling at runtime without redeploying:

@app.function()
def scale_up_for_peak():
    process = modal.Function.from_name("my-app", "process")
    process.update_autoscaler(min_containers=10, buffer_containers=20)

@app.function()
def scale_down_after_peak():
    process = modal.Function.from_name("my-app", "process")
    process.update_autoscaler(min_containers=1, buffer_containers=2)

Settings revert to the decorator values on the next deployment. with_options(), with_concurrency() and with_batching() instead create variants with separate container pools. A base max_containers does not cap the sum of variants; avoid high-cardinality configurations. Variants ignore base min_containers, and with_options() does not accept that parameter.

Raw lines 163 to 184
185## Limits
187| Resource | Limit |
188|----------|-------|
189| Queued synchronous inputs | 2,000 |
190| Total synchronous inputs (running + queued) | 25,000 |
191| Pending `.spawn()` inputs | 1,000,000 |
192| Concurrent inputs per `.map()` | 1,000 |
193| Containers per Function | 4,000, subject to lower workspace/GPU quotas |
194| Baseline synchronous invocation rate | 200/s |
195| Baseline asynchronous invocation rate | 1,500/s |
197These are documented platform limits at review time, not guaranteed capacity.
198SDK calls can raise `modal.exception.ResourceExhaustedError`; Web Functions can
199return HTTP 429. Use bounded backoff and idempotent work, and check workspace quotas.

Limits

ResourceLimit
Queued synchronous inputs2,000
Total synchronous inputs (running + queued)25,000
Pending .spawn() inputs1,000,000
Concurrent inputs per .map()1,000
Containers per Function4,000, subject to lower workspace/GPU quotas
Baseline synchronous invocation rate200/s
Baseline asynchronous invocation rate1,500/s

These are documented platform limits at review time, not guaranteed capacity. SDK calls can raise modal.exception.ResourceExhaustedError; Web Functions can return HTTP 429. Use bounded backoff and idempotent work, and check workspace quotas.

Raw lines 185 to 199
Starts an agent spawn :191