mrkeyoor.com_
Thu 06 Aug 10:54 UTC
PyPIInfraupdated 06 Aug 2026

modal

modal is the Python SDK and CLI for Modal, a hosted serverless compute platform. You decorate an ordinary Python function with @app.function(), describe the container it should run in with modal.Image, and calling my_function.remote(x) ships the arguments to Modal's cloud, starts a container there, runs the function, and returns the result to your laptop. The same decorator covers GPU jobs, cron schedules, HTTP endpoints, and fan-out across thousands of containers with .map(). There is no Dockerfile to write, no Kubernetes manifest, and no cluster to keep alive; the image is defined in Python and built on their infrastructure. The important thing to understand before you install it: this package is a client. It does nothing on its own. Every meaningful call goes over gRPC to modal.com, needs an account and a token, and gets billed per second of container time.

Verdict

For bursty GPU work and fan-out batch jobs, Modal removes more infrastructure work than anything else with a Python API, and the developer experience is genuinely good. Go in clear-eyed that you are writing code against one vendor's platform with no exit path and a per-second bill, which is a strategic choice, not just a library choice.

API stability3/5Since 1.0 the surface has settled and semantic versioning is now promised, but the 1.0 migration removed Stub and renamed keep_warm, concurrency_limit, container_idle_timeout, allow_concurrent_inputs, and web_endpoint, so anything written before mid-2025 breaks and the server side can change under a pinned client.
Docs4/5modal.com/docs has a task-shaped guide, a full Python reference, and a large examples gallery that is kept current; the GitHub README is nearly empty, docstrings carry most of the detail, and pricing behaviour around warm containers and cold starts is thinner than it should be.
Maintenance5/5The repo was pushed 6 August 2026, 1.5.3 shipped 23 July 2026, dev builds go out most nights, and only 12 issues are open (21 counting PRs) because it is a commercial team maintaining the client for their own paid product.
Ecosystem3/5Around 23.6M weekly PyPI downloads and official JavaScript and Go SDKs alongside the Python one, but the GitHub repo has 496 stars and there is no third-party plugin ecosystem to speak of: everything of value lives behind the vendor's API rather than in community packages.

Use it if

  • You need GPUs occasionally and do not want to own them. Requesting gpu='A100' on a decorator and paying for the minutes you use beats renting an instance that idles overnight
  • Your workload is embarrassingly parallel. .map() over ten thousand inputs spreads across containers with no queue, worker pool, or autoscaler for you to operate
  • You want the container image defined in the same file as the code, with layer caching handled remotely: Image.debian_slim().apt_install('ffmpeg').pip_install('torch') replaces a Dockerfile plus a registry plus a build pipeline
  • You are shipping an inference or batch-job endpoint and the interesting engineering is the model, not the infrastructure; @modal.fastapi_endpoint() gives you a URL from a Python function
  • You need scheduled jobs and web endpoints from the same codebase without standing up a scheduler and a web service separately
Skip it if

Setup reality

pip install modal, then run modal setup, which opens a browser to create an API token and writes it to ~/.modal.toml; in CI you set MODAL_TOKEN_ID and MODAL_TOKEN_SECRET as environment variables instead. That much is smooth. The friction starts with the dependency set: aiohttp, grpclib, protobuf, rich, click, watchfiles, cbor2, toml, and synchronicity all land in your environment, and the pins on grpclib and protobuf are tight enough to conflict with anything else that speaks gRPC. Version 1.0 renamed a lot of things and the old names are gone or deprecated: modal.Stub is now modal.App and raises an AttributeError telling you so, @modal.web_endpoint is now @modal.fastapi_endpoint, keep_warm became min_containers, concurrency_limit became max_containers, container_idle_timeout became scaledown_window, and allow_concurrent_inputs was replaced by the @modal.concurrent decorator. Any tutorial written before mid-2025 will not run. The mental model also takes a while: code at module level runs both locally and inside the container, so imports of heavy libraries belong inside the function body or inside image.imports(), and files are not shipped unless you add them with add_local_dir or add_local_python_source. Finally, note the release cadence: PyPI carries over 2600 releases including a dev build most nights, so pin an exact version in production rather than tracking the latest.

Patterns

Run a Python function in the cloudfirst-remote-function

# app.py
import modal

app = modal.App("example")

@app.function()
def square(x: int) -> int:
    return x * x

@app.local_entrypoint()
def main():
    print(square.remote(4))   # runs on Modal
    print(square.local(4))    # runs on your machine

# modal run app.py

Module-level code runs both locally and in the container, so keep heavy imports inside the function. .remote() blocks until the result comes back, .local() skips Modal entirely, and calling square(4) directly raises rather than guessing which you meant.

Describe the container image in Pythondefine-image

image = (
    modal.Image.debian_slim(python_version="3.12")
    .apt_install("ffmpeg", "git")
    .pip_install("torch==2.5.1", "transformers")
    .env({"HF_HUB_ENABLE_HF_TRANSFER": "1"})
    .add_local_python_source("my_package")
)

@app.function(image=image)
def run():
    import torch
    return torch.__version__

Each call adds a cached layer, so put slow steps first and volatile ones last. Your local files are not in the image unless you add them: add_local_python_source for modules, add_local_dir for data. There is also Image.from_registry(tag) for an existing Docker image, which must be linux/amd64.

Ask for a GPU and a longer timeoutgpu-function

@app.function(
    image=image,
    gpu="A100-40GB",
    timeout=60 * 30,
    retries=modal.Retries(max_retries=2, backoff_coefficient=2.0),
    memory=16384,
)
def train(batch: list[str]) -> str:
    import torch
    assert torch.cuda.is_available()
    return "done"

timeout defaults to 300 seconds, which kills long training runs without much explanation. gpu also accepts a list such as ['H100', 'A100'] so the scheduler can fall back when your first choice has no capacity.

Fan out across many containersparallel-map

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

    # arguments as tuples
    list(add.starmap([(1, 2), (3, 4)]))

    # fire and forget, ignore return values
    square.for_each(range(1000))

    # detach one call and poll for it later
    call = square.spawn(7)
    print(call.get(timeout=60))

map() returns a lazy generator, so nothing runs until you iterate it. Container count is bounded by max_containers on the decorator; without one, a large map can start a very large number of containers and the bill scales with them.

Load a model once per container with a classclass-with-startup

@app.cls(image=image, gpu="L4", scaledown_window=300)
class Embedder:
    model_name: str = modal.parameter(default="BAAI/bge-small-en")

    @modal.enter()
    def load(self):
        from sentence_transformers import SentenceTransformer
        self.model = SentenceTransformer(self.model_name)

    @modal.method()
    def embed(self, texts: list[str]):
        return self.model.encode(texts).tolist()

    @modal.exit()
    def shutdown(self):
        del self.model

@app.local_entrypoint()
def main():
    print(Embedder().embed.remote(["hello"]))

@modal.enter() runs once when a container starts, not once per call, which is the whole point for model loading. Class attributes must be declared with modal.parameter() to become constructor arguments; a plain __init__ is not how you pass configuration here.

Expose a function as a URLhttp-endpoint

web_image = modal.Image.debian_slim().pip_install("fastapi[standard]")

@app.function(image=web_image)
@modal.fastapi_endpoint(method="POST", docs=True)
def predict(item: dict):
    return {"ok": True, "echo": item}

# modal serve app.py   -> temporary URL that hot reloads
# modal deploy app.py  -> permanent URL

Decorator order matters: @app.function() goes outside, @modal.fastapi_endpoint() inside. This replaced @modal.web_endpoint, which is still importable but deprecated. fastapi is not in the default image, so add it or the deploy fails at build time.

Serve a whole FastAPI appasgi-app

from fastapi import FastAPI

web_app = FastAPI()

@web_app.get("/health")
def health():
    return {"status": "ok"}

@app.function(image=web_image)
@modal.asgi_app()
def fastapi_app():
    return web_app

The decorated function is a factory: it runs inside the container and returns the ASGI app, so build the app at module level or inside the function, but do not import server-only dependencies at the top of a file you also run locally.

Inject credentials without committing themsecrets

@app.function(
    secrets=[
        modal.Secret.from_name("openai-key", required_keys=["OPENAI_API_KEY"]),
        modal.Secret.from_dict({"STAGE": "prod"}),
    ]
)
def call_api():
    import os
    return os.environ["OPENAI_API_KEY"][:6]

# modal secret create openai-key OPENAI_API_KEY=sk-...

Secrets arrive as environment variables inside the container only. required_keys makes a missing key fail at deploy time instead of with a KeyError in the middle of a run. from_dict is for non-sensitive config; it is visible in your source.

Keep files between runs with a Volumepersistent-volume

vol = modal.Volume.from_name("model-cache", create_if_missing=True)

@app.function(image=image, volumes={"/cache": vol})
def download():
    from pathlib import Path
    Path("/cache/weights.bin").write_bytes(b"...")
    vol.commit()   # make it visible to other containers

@app.function(volumes={"/cache": vol})
def read():
    vol.reload()
    return open("/cache/weights.bin", "rb").read()

Writes are not durable until commit(), and other containers do not see them until they reload(). reload() fails if the volume has open file handles. Volumes are tuned for a moderate number of large files, not for many small ones.

Run something on a schedulescheduled-job

@app.function(schedule=modal.Cron("0 9 * * *", timezone="Asia/Kolkata"))
def daily_report():
    ...

@app.function(schedule=modal.Period(hours=4))
def every_four_hours():
    ...

# modal deploy app.py

Schedules only fire for a deployed app; modal run executes once and exits. Cron takes a timezone, Period does not and simply measures the interval from the previous run. A scheduled function cannot take arguments.

Invoke a deployed function from another processcall-deployed-code

import modal

f = modal.Function.from_name("example", "square")
print(f.remote(4))

Embedder = modal.Cls.from_name("example", "Embedder")
print(Embedder().embed.remote(["hello"]))

q = modal.Queue.from_name("jobs", create_if_missing=True)
q.put({"id": 1})

This is how a normal web server or worker calls into Modal without defining the app locally. Lookups are lazy and resolve on first use, so a wrong app or function name surfaces as a NotFoundError at call time rather than at import.

Control input concurrency and container countsconcurrency-and-scaling

@app.function(
    min_containers=1,      # keep one warm, you pay for it
    max_containers=50,     # hard ceiling on fan-out
    buffer_containers=2,   # spin up ahead of demand
    scaledown_window=120,  # seconds idle before shutdown
)
@modal.concurrent(max_inputs=10, target_inputs=6)
async def handle(req: dict):
    ...

@modal.concurrent replaced the allow_concurrent_inputs parameter and only helps for IO-bound work; running ten CPU-bound inputs in one container just makes each slower. min_containers is the old keep_warm and is the fastest way to run up a bill you did not expect.

Alternatives

PackageRegistryPick it when
runpodPyPIYou mainly want cheap on-demand GPU containers and are happy managing your own Docker image and handler contract
rayPyPIYou want the same remote-function and fan-out model but running on infrastructure you control, on-prem or in your own cloud account
bentomlPyPIYou are packaging and serving models and want an open-source framework that deploys to Kubernetes or any cloud rather than one vendor
prefectPyPIThe actual need is scheduled and retried batch workflows rather than on-demand GPU compute