Lambda MicroVMs
A Lambda MicroVM is an isolated, stateful virtual machine you start on demand, one per user or session. It boots from a MicroVM image: a Firecracker snapshot of your application, already started and initialized, so a new MicroVM is ready in milliseconds. That makes MicroVMs a fit for per-user sandboxes: code interpreters, agent workspaces, preview environments.
The model is image, then launch. AWS.Lambda.MicrovmImage builds the image once, at deploy time. A Lambda Function then launches MicroVMs from it while it runs, and drives each one over HTTP or typed RPC.
The image is built on AWS, so you need no local Docker, from one of three sources. Give it main, and Alchemy bundles your TypeScript server and writes its Dockerfile. Give it context and a dockerfile, and it builds your own app in any language. Or give it codeArtifact, a zip in S3 or an image in ECR you’ve already built.
Define an effectful image
Section titled “Define an effectful image”Write the in-VM server as an Effect. The class is a typed handle the
orchestrator imports; the default-exported .make() is what gets bundled into
the image. The shape can expose both a typed RPC method (hello) and a
raw fetch route — the in-VM runtime serves RPC over an internal
/__rpc__/* protocol and falls through to fetch for everything else. The
{ fetch, ...rpcs } shape is Schemaless RPC — the same
contract every Function and Server returns:
import * as AWS from "@alchemy.run/aws";import * as Effect from "effect/Effect";import { HttpServerRequest } from "effect/http/HttpServerRequest";import * as HttpServerResponse from "effect/http/HttpServerResponse";
export const BuildRole = AWS.IAM.Role("MicrovmBuildRole");
export class Sandbox extends AWS.Lambda.MicrovmImage< Sandbox, { hello: (message: string) => Effect.Effect<string> }>()("Sandbox") {}
export default Sandbox.make( BuildRole.pipe( Effect.map((buildRole) => ({ main: import.meta.filename, buildRole })), ), Effect.gen(function* () { return { // typed RPC method — reached with `connectMicrovm` hello: (message: string) => Effect.succeed(`hello, ${message}!`), // raw HTTP route — reached with a plain HTTPS request to the endpoint fetch: Effect.gen(function* () { const request = yield* HttpServerRequest; const url = new URL(request.url, "http://microvm"); return yield* HttpServerResponse.json({ message: url.searchParams.get("message") ?? "", }); }), }; }),);The bare Role passed as buildRole needs no policy of its own — the image
grants it the Lambda trust statement plus the S3 and CloudWatch-logs
permissions the build needs via a binding.
Launch and drive MicroVMs from a Lambda
Section titled “Launch and drive MicroVMs from a Lambda”The image is just the template. A Lambda Function binds the per-instance
lifecycle operations — RunMicrovm, GetMicrovm, CreateAuthToken,
TerminateMicrovm — each scoped to the image by IAM automatically. Launch a
VM, wait for RUNNING, mint an auth token, then call the typed RPC stub with
connectMicrovm:
import * as AWS from "@alchemy.run/aws";import * as Duration from "effect/Duration";import * as Effect from "effect/Effect";import * as Layer from "effect/Layer";import * as Schedule from "effect/Schedule";import * as FetchHttpClient from "effect/http/FetchHttpClient";import * as HttpServerResponse from "effect/http/HttpServerResponse";import { Sandbox } from "./sandbox.ts";
export default class Api extends AWS.Lambda.Function<Api>()( "Api", // generous timeout: the handler waits for the MicroVM to reach RUNNING { main: import.meta.filename, functionUrl: true, timeout: Duration.seconds(120) }, Effect.gen(function* () { const runMicrovm = yield* AWS.Lambda.RunMicrovm(Sandbox); const getMicrovm = yield* AWS.Lambda.GetMicrovm(Sandbox); const createAuthToken = yield* AWS.Lambda.CreateAuthToken(Sandbox); const terminateMicrovm = yield* AWS.Lambda.TerminateMicrovm(Sandbox);
return { fetch: Effect.gen(function* () { const vm = yield* runMicrovm({}); return yield* Effect.gen(function* () { // wait until the MicroVM is RUNNING before connecting yield* getMicrovm({ microvmIdentifier: vm.microvmId }).pipe( Effect.flatMap((m) => m.state === "RUNNING" ? Effect.void : Effect.fail(new Error(`microvm ${m.state}`)), ), Effect.retry({ schedule: Schedule.spaced("2 seconds"), times: 30 }), ); const { authToken } = yield* createAuthToken({ microvmIdentifier: vm.microvmId, expirationInMinutes: 5, allowedPorts: [{ port: 8080 }], // the in-VM server's port }); const sandbox = yield* AWS.Lambda.connectMicrovm(Sandbox, { endpoint: vm.endpoint, authToken, }); const reply = yield* sandbox.hello("world"); // "hello, world!" return yield* HttpServerResponse.json({ reply }); }).pipe( // terminate on success OR failure — never leak a running MicroVM Effect.ensuring( terminateMicrovm({ microvmIdentifier: vm.microvmId }).pipe( Effect.ignore, ), ), // the in-VM endpoint calls need an HttpClient for this scope Effect.provide(FetchHttpClient.layer), ); }), }; }).pipe( Effect.provide( Layer.mergeAll( AWS.Lambda.RunMicrovmHttp, AWS.Lambda.GetMicrovmHttp, AWS.Lambda.CreateAuthTokenHttp, AWS.Lambda.TerminateMicrovmHttp, ), ), ),) {}The stub is Schemaless RPC — the same typed client you get binding a Durable Object or Container into a Worker, here a MicroVM into a Lambda Function.
Always wrap the work in Effect.ensuring(terminateMicrovm(...)) — a failure
(or a client retry) must never leak a running MicroVM against your account’s
memory quota. SuspendMicrovm, ResumeMicrovm, and ListMicrovms follow the
same bind-then-call pattern for longer-lived sessions.
Call the fetch route directly
Section titled “Call the fetch route directly”The fetch handler is plain HTTPS against the MicroVM’s endpoint (a bare
hostname like <id>.lambda-microvm.<region>.on.aws). Authenticate with the
same token via microvmAuthHeaders:
const client = yield* HttpClient.HttpClient;const res = yield* client.get(`https://${vm.endpoint}/echo?message=hi`, { headers: AWS.Lambda.microvmAuthHeaders(authToken),});const body = yield* res.json; // { message: "hi" }Both surfaces coexist on one image: the RPC stub for typed calls from your own code, the fetch route for anything that speaks HTTP.
Wire the stack
Section titled “Wire the stack”The Lambda pulls in the Sandbox class handle; the image’s .make() runtime
is provided on the Stack so it gets built and deployed alongside:
import * as Alchemy from "alchemy";import * as AWS from "@alchemy.run/aws";import * as Effect from "effect/Effect";import Api from "./api.ts";import SandboxLive from "./sandbox.ts";
export default Alchemy.Stack( "MicrovmApp", { providers: AWS.providers(), state: AWS.state() }, Effect.gen(function* () { const api = yield* Api; return { url: api.functionUrl.as<string>() }; }).pipe(Effect.provide(SandboxLive)),);Bring your own Dockerfile
Section titled “Bring your own Dockerfile”For non-TypeScript workloads, point context at a directory containing a
Dockerfile (built FROM public.ecr.aws/lambda/microvms:al2023-minimal) and
the files it copies. Alchemy zips and uploads the directory; AWS runs the
Dockerfile server-side. There is no in-VM Effect runtime to bind, so both
the bare build Role and the image live directly in the Stack’s Effect.gen:
import * as Alchemy from "alchemy";import * as AWS from "@alchemy.run/aws";import * as Effect from "effect/Effect";
export default Alchemy.Stack( "FlaskMicrovm", { providers: AWS.providers(), state: AWS.state() }, Effect.gen(function* () { const buildRole = yield* AWS.IAM.Role("ExternalBuildRole", {}); const image = yield* AWS.Lambda.MicrovmImage("Flask", { context: `${import.meta.dirname}/app`, // dir with Dockerfile + app.py buildRole, }); return { imageArn: image.imageArn.as<string>() }; }),);Re-deploys only trigger a new build when the artifact’s content hash or a build-affecting prop changes; otherwise the image is left untouched.
Where next
Section titled “Where next”- Lambda — the Function model the orchestrator is built on.
- MicrovmImage API reference — all props,
build modes, sizing, logging, and VPC egress via
NetworkConnector. - RunMicrovm and the other per-instance lifecycle bindings.