Skip to content

Servers

A Modal.Server runs your program as an HTTP server. Requests are routed through one region, and Modal scales containers on concurrent requests.

Each container serves many requests at once

Section titled “Each container serves many requests at once”

A Function’s container takes one call at a time unless you raise it. A Server’s container serves requests side by side, and Modal adds a container when they pass its target:

container 1GET /GET /aGET /bcontainer 2GET /cpast the targetcontainer 1GET /GET /aGET /bcontainer 2GET /cpast the target
requestcold start
Container 1 serves three requests at once. When concurrency passes the target, Modal starts container 2.
src/Api.ts
import * as Modal from "@alchemy.run/modal";
import * as Effect from "effect/Effect";
import * as HttpServerResponse from "effect/http/HttpServerResponse";
import { Backend } from "./Backend.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")) };
}),
) {}
src/Gateway.ts
import * as Cloudflare from "@alchemy.run/cloudflare";
import * as Modal from "@alchemy.run/modal";
import * as Effect from "effect/Effect";
import * as HttpClientRequest from "effect/http/HttpClientRequest";
import * as HttpServerResponse from "effect/http/HttpServerResponse";
import Api from "./Api.ts";
export default class Gateway extends Cloudflare.Worker<Gateway>()(
"Gateway",
{ main: import.meta.url },
Effect.gen(function* () {
const api = yield* Modal.FetchServer(Api);
return {
fetch: Effect.gen(function* () {
const response = yield* api.fetch(HttpClientRequest.get("/"));
return HttpServerResponse.text(yield* response.text);
}).pipe(Effect.orDie),
};
}).pipe(Effect.provide(Modal.FetchServerHttp)),
) {}

Servers require Modal’s proxy auth. The binding creates a Modal.WebhookToken for the Worker and sends its headers with every request.

Effect.gen(function* () {
return {
greet: Effect.fn(function* (name: string) {
return `hello ${name}`;
}),
fetch: Effect.succeed(HttpServerResponse.text("ok")),
};
}),

Every function the program returns besides fetch is an RPC method, served at POST /__rpc__/<method> next to your routes.

const api = yield* Modal.FetchServer(Api);
const greeting = yield* api.greet("modal");

The client is typed from Api’s methods and goes through Modal’s proxy auth like fetch. The binding also gives the Worker its own key, which the Server checks on every method call: callers without a binding get 401. A method that returns a Stream streams to the caller as values are produced. See schemaless RPC for what methods can take and return.

src/Relay.ts
import * as Modal from "@alchemy.run/modal";
import * as Effect from "effect/Effect";
import Api from "./Api.ts";
import { Backend } from "./Backend.ts";
export default class Relay extends Modal.Function<Relay>()(
"Relay",
{ main: import.meta.url, app: Backend },
Effect.gen(function* () {
const api = yield* Modal.FetchServer(Api);
return {
relay: Effect.fn(function* (name: string) {
return yield* api.greet(name);
}),
};
}).pipe(Effect.provide(Modal.FetchServerHttp)),
) {}

The binding is the same one a Worker uses. A Server sits behind proxy auth even for programs inside Modal, so Relay still gets its own Modal.WebhookToken, and its method calls carry the key its binding issued.

Callers without a binding reach only fetch

Section titled “Callers without a binding reach only fetch”

Any caller with a proxy auth token Modal accepts for the Server can reach the Server’s fetch routes. Only callers whose binding issued a key can call its methods: everyone else gets 401.

{ main: import.meta.url, app: Backend, unauthenticated: true }

An unauthenticated Server serves only fetch. Its RPC methods answer 404, so the public can’t call them.

{
main: import.meta.url,
app: Backend,
minContainers: 2,
targetConcurrency: 20,
maxConcurrency: 50,
exitGracePeriod: "30 seconds",
}

Each container aims for 20 requests at once and turns away more than 50 with 429.

src/Api.ts
import * as Modal from "@alchemy.run/modal";
import { Backend } from "./Backend.ts";
export const Api = Modal.Server("Api", {
main: "./src/api-server.ts",
app: Backend,
unauthenticated: true,
});
src/api-server.ts
Bun.serve({ port: Number(process.env.PORT), fetch: () => new Response("ok") });