Skip to content

What is Modal?

Modal runs your code in containers, with GPUs when you ask for them. A workspace holds everything you create and pays for it. It’s divided into environments, and in each environment, apps group the code that runs: Functions and Servers that Modal scales with traffic, and Sandboxes your code creates while it runs. Each of them starts from an image. Volumes, Dicts, Queues and Secrets sit beside the apps in the same environment. You pay per second for the containers that run, and nothing while they’re scaled to zero.

Take one small app: a Cloudflare Worker gives each user a Sandbox from the Warm image, with a Volume of their own as /home/user. An Embed Function on an L4 GPU turns their text into vectors. The Coder and Inference apps hold the code, and the MyEnv environment holds both apps with their Volume and Secret.

A workspace owns your apps, storage and secrets, its members, and the bill. A personal account is a workspace of one, and a team shares one.

Modal authenticates calls with a token: a token id (ak-…) and a secret (as-…). A token acts on the whole workspace, so Alchemy deploys with yours, and gives each runtime that binds a Modal object a service user of its own, with only the role its bindings need.

Terminal window
pnpm alchemy profile edit --add Modal

See Workspaces & tokens.

Environments: stages that never share a Volume

Section titled “Environments: stages that never share a Volume”

An environment divides a workspace. Two objects with the same name in two environments are different objects, so dev and prod never share a Volume by accident. Every workspace starts with one called main.

src/environments.ts
export const MyEnv = Modal.Environment("MyEnv", {});

Alchemy names the environment from your app, stage and id, so each stage gets its own. Everything else you declare names its environment with environment: MyEnv, or lands in the workspace’s default one.

See Workspaces & tokens.

An app groups the code you deploy together and acts as its namespace. A deployed app runs until you stop it, and stopping it stops every Function, Server and Sandbox in it.

src/apps.ts
export const Coder = Modal.App("Coder", { environment: MyEnv });

Images: the filesystem a container starts from

Section titled “Images: the filesystem a container starts from”

An image is the filesystem a container starts from. Modal builds it on its own builders, from a registry image, a Dockerfile, or another image with commands run on top, so a deploy needs no Docker on your machine.

src/images.ts
export const Node = Modal.Image("Node", { app: Coder, image: "node:22-bookworm" });
export const Warm = Modal.Image("Warm", {
app: Coder,
from: Node,
run: ["git clone --depth 1 https://github.com/sindresorhus/is /work"],
});

Warm runs its setup once at deploy and saves the result, so every container from it starts with the repository already there.

See Images.

Functions: containers that scale with calls

Section titled “Functions: containers that scale with calls”

A Function is code Modal runs when it’s called. Each call is an input. Modal starts containers as inputs arrive, runs several at once when calls pile up, and stops them once they’ve been idle for the scale-down window. With no inputs, nothing runs and nothing is billed.

container 1embedembedidlecontainer 2embedidlecalls pile upcontainer 1embedembedidlecontainer 2embedidlecalls pile up
callcold startidle
The first call starts a container. When calls arrive faster than one container serves them, Modal starts another. Each stops after it has been idle for the scale-down window, and with no calls nothing runs.

In Alchemy, a Function is a class that holds its own code. Every function it returns is a method callers reach by name:

src/embed.ts
export default class Embed extends Modal.Function<Embed>()(
"Embed",
{ main: import.meta.url, app: Inference, gpu: "L4" },
Effect.gen(function* () {
return {
embed: Effect.fn(function* (text: string) {
return { tokens: text.split(/\s+/).length };
}),
};
}),
) {}

A Function can also run on a schedule, or serve its fetch handler at a *.modal.run URL as a web endpoint.

See Functions and Web endpoints.

Servers: HTTP that scales with concurrency

Section titled “Servers: HTTP that scales with concurrency”

A Server is a long-running HTTP server. Requests reach it through one routing region, and Modal scales its containers on how many requests each is serving at once, not on a queue of calls.

src/api.ts
export default class Api extends Modal.Server<Api>()(
"Api",
{ main: import.meta.url, app: Backend, routingRegion: "us-east" },
Effect.gen(function* () {
return { fetch: Effect.succeed(HttpServerResponse.text("ok")) };
}),
) {}

See Servers and Choosing a runtime.

Sandboxes: containers your code creates on the fly

Section titled “Sandboxes: containers your code creates on the fly”

A Sandbox is a container your code creates while it runs, to execute code it doesn’t trust: an agent’s commands, a user’s script. It runs until its timeout (five minutes unless you ask for up to 24 hours) or until it’s been idle for its idle timeout, and you run commands in it, read and write its files, and reach its ports through tunnels.

const sandboxes = yield* Modal.CreateSandbox(Warm);
const sandbox = yield* sandboxes.create({ timeout: "10 minutes" });
const result = yield* sandbox.exec(["ls", "/work"]);

A Sandbox’s filesystem, or its memory too, can be snapshotted and started again elsewhere.

See Sandboxes and Snapshots.

Volumes, Dicts and Queues: state between containers

Section titled “Volumes, Dicts and Queues: state between containers”

A Volume is a filesystem that Functions, Servers and Sandboxes mount at a path, and that outlives every container that mounts it. A Dict is a key-value store and a Queue hands work between processes; both hold JSON and expire their entries.

src/storage.ts
export const Weights = Modal.Volume("Weights", { environment: MyEnv });
export const Results = Modal.Dict("Results", { environment: MyEnv });
export const Jobs = Modal.Queue("Jobs", { environment: MyEnv });

A Volume per user is created by name while your app runs:

const volumes = yield* Modal.CreateVolume(MyEnv);
const home = yield* volumes.getOrCreate(`home-${user}`);

See Volumes, Dicts and Queues.

A Secret is a named set of environment variables, stored in an environment and attached to the containers that need them. A Sandbox can also keep a Secret out of its environment entirely: Modal adds it to outgoing requests for the hosts you name, so code inside never sees it.

src/secrets.ts
export const HuggingFace = Modal.Secret("HuggingFace", {
environment: MyEnv,
env: { HF_TOKEN: Config.Redacted("HF_TOKEN") },
});

See Secrets.

A Sandbox reaches the internet unless you limit it. Block its network, or allow only some domains and address ranges, and change either while it runs. Its ports reach the outside through tunnels, at a public URL per port.

const sandbox = yield* sandboxes.create({ allowedDomains: ["github.com"], encryptedPorts: [3000] });

See Network & tunnels.

Modal has no storage for Alchemy’s own record of what each Stack created. Keep it on disk with Alchemy.localState(), or share it with your team and CI through Cloudflare.state() or AWS.state().

How @alchemy.run/modal models these concepts

Section titled “How @alchemy.run/modal models these concepts”

Your profile holds a workspace token, and Modal.providers() deploys with it. Environments, apps, images, Volumes, Dicts, Queues and Secrets an app shares are resources in the Stack, and Functions, Servers and Sandbox programs are classes that hold their own code. Sandboxes, Volumes, Secrets and images made per user or task come from bindings your runtime uses while it runs (CreateSandbox, CreateVolume, CreateSecret, CreateImage). They carry their stack, stage and binding in tags or a name prefix, and stay when the Stack is destroyed. Each runtime that binds a Modal object gets a service user of its own, never your token.

Next, build one: the tutorial puts these ideas to work, one step at a time.