1---
2title: Lambda MicroVMs
3description: Build a Firecracker MicroVM image from TypeScript or a Dockerfile, then launch and drive isolated stateful instances from a Lambda Function with typed lifecycle bindings and RPC.
4sidebar:
5 order: 8
6---
7
8A Lambda **MicroVM image** is a Firecracker snapshot that boots a fully
9initialized application in milliseconds. The model is *image-then-launch*:
10`AWS.Lambda.MicrovmImage` builds the image once at deploy time, then a Lambda
11Function launches isolated, stateful MicroVM instances from it at runtime —
12one per end-user or session. That makes MicroVMs a natural fit for per-user
13sandboxes: code interpreters, agent workspaces, preview environments.
14
15The build runs **server-side on AWS** — no local Docker. Alchemy produces the
16code artifact in one of three modes, selected by which prop you set:
17
18- **Effectful** (`main`) — write the in-VM HTTP server in TypeScript; Alchemy
19 bundles it, generates a Dockerfile, and uploads the zip.
20- **External** (`context`/`dockerfile`) — bring your own Dockerfile and build
21 context in any language; Alchemy zips and uploads the directory.
22- **Prebuilt** (`codeArtifact`) — point at an existing S3 zip or ECR image URI.
23
24:::caution
25Lambda MicroVMs are a **preview feature**: your account must be onboarded to
26the preview, and effectful/external builds need a bootstrapped Assets bucket
27(`alchemy provider aws bootstrap`). Builds are asynchronous and can take several
28minutes — the provider polls until the image reaches `CREATED` and streams
29build logs to CloudWatch.
30:::
31
32## Define an effectful image
33
34Write the in-VM server as an Effect. The class is a typed handle the
35orchestrator imports; the default-exported `.make()` is what gets bundled into
36the image. The shape can expose both a typed **RPC method** (`hello`) and a
37raw **`fetch`** route — the in-VM runtime serves RPC over an internal
38`/__rpc__/*` protocol and falls through to `fetch` for everything else. The
39`{ fetch, ...rpcs }` shape is [Schemaless RPC](/apis/schemaless) — the same
40contract every Function and Server returns:
41
42```typescript
43// sandbox.ts
44import * as AWS from "alchemy/AWS";
45import * as Effect from "effect/Effect";
46import { HttpServerRequest } from "effect/http/HttpServerRequest";
47import * as HttpServerResponse from "effect/http/HttpServerResponse";
48
49export const BuildRole = AWS.IAM.Role("MicrovmBuildRole");
50
51export class Sandbox extends AWS.Lambda.MicrovmImage<
52 Sandbox,
53 { hello: (message: string) => Effect.Effect<string> }
54>()("Sandbox") {}
55
56export default Sandbox.make(
57 BuildRole.pipe(
58 Effect.map((buildRole) => ({ main: import.meta.filename, buildRole })),
59 ),
60 Effect.gen(function* () {
61 return {
62 // typed RPC method — reached with `connectMicrovm`
63 hello: (message: string) => Effect.succeed(`hello, ${message}!`),
64 // raw HTTP route — reached with a plain HTTPS request to the endpoint
65 fetch: Effect.gen(function* () {
66 const request = yield* HttpServerRequest;
67 const url = new URL(request.url, "http://microvm");
68 return yield* HttpServerResponse.json({
69 message: url.searchParams.get("message") ?? "",
70 });
71 }),
72 };
73 }),
74);
75```
76
77The bare `Role` passed as `buildRole` needs no policy of its own — the image
78grants it the Lambda trust statement plus the S3 and CloudWatch-logs
79permissions the build needs via a binding.
80
81## Launch and drive MicroVMs from a Lambda
82
83The image is just the template. A Lambda Function binds the per-instance
84lifecycle operations — `RunMicrovm`, `GetMicrovm`, `CreateAuthToken`,
85`TerminateMicrovm` — each scoped to the image by IAM automatically. Launch a
86VM, wait for `RUNNING`, mint an auth token, then call the typed RPC stub with
87`connectMicrovm`:
88
89```typescript
90// api.ts
91import * as AWS from "alchemy/AWS";
92import * as Duration from "effect/Duration";
93import * as Effect from "effect/Effect";
94import * as Layer from "effect/Layer";
95import * as Schedule from "effect/Schedule";
96import * as FetchHttpClient from "effect/http/FetchHttpClient";
97import * as HttpServerResponse from "effect/http/HttpServerResponse";
98import { Sandbox } from "./sandbox.ts";
99
100export default class Api extends AWS.Lambda.Function<Api>()(
101 "Api",
102 // generous timeout: the handler waits for the MicroVM to reach RUNNING
103 { main: import.meta.filename, functionUrl: true, timeout: Duration.seconds(120) },
104 Effect.gen(function* () {
105 const runMicrovm = yield* AWS.Lambda.RunMicrovm(Sandbox);
106 const getMicrovm = yield* AWS.Lambda.GetMicrovm(Sandbox);
107 const createAuthToken = yield* AWS.Lambda.CreateAuthToken(Sandbox);
108 const terminateMicrovm = yield* AWS.Lambda.TerminateMicrovm(Sandbox);
109
110 return {
111 fetch: Effect.gen(function* () {
112 const vm = yield* runMicrovm({});
113 return yield* Effect.gen(function* () {
114 // wait until the MicroVM is RUNNING before connecting
115 yield* getMicrovm({ microvmIdentifier: vm.microvmId }).pipe(
116 Effect.flatMap((m) =>
117 m.state === "RUNNING"
118 ? Effect.void
119 : Effect.fail(new Error(`microvm ${m.state}`)),
120 ),
121 Effect.retry({ schedule: Schedule.spaced("2 seconds"), times: 30 }),
122 );
123 const { authToken } = yield* createAuthToken({
124 microvmIdentifier: vm.microvmId,
125 expirationInMinutes: 5,
126 allowedPorts: [{ port: 8080 }], // the in-VM server's port
127 });
128 const sandbox = yield* AWS.Lambda.connectMicrovm(Sandbox, {
129 endpoint: vm.endpoint,
130 authToken,
131 });
132 const reply = yield* sandbox.hello("world"); // "hello, world!"
133 return yield* HttpServerResponse.json({ reply });
134 }).pipe(
135 // terminate on success OR failure — never leak a running MicroVM
136 Effect.ensuring(
137 terminateMicrovm({ microvmIdentifier: vm.microvmId }).pipe(
138 Effect.ignore,
139 ),
140 ),
141 // the in-VM endpoint calls need an HttpClient for this scope
142 Effect.provide(FetchHttpClient.layer),
143 );
144 }),
145 };
146 }).pipe(
147 Effect.provide(
148 Layer.mergeAll(
149 AWS.Lambda.RunMicrovmHttp,
150 AWS.Lambda.GetMicrovmHttp,
151 AWS.Lambda.CreateAuthTokenHttp,
152 AWS.Lambda.TerminateMicrovmHttp,
153 ),
154 ),
155 ),
156) {}
157```
158
159The stub is [Schemaless RPC](/apis/schemaless) — the same typed client you get
160binding a Durable Object or Container into a Worker, here a MicroVM into a
161Lambda Function.
162
163Always wrap the work in `Effect.ensuring(terminateMicrovm(...))` — a failure
164(or a client retry) must never leak a running MicroVM against your account's
165memory quota. `SuspendMicrovm`, `ResumeMicrovm`, and `ListMicrovms` follow the
166same bind-then-call pattern for longer-lived sessions.
167
168## Call the fetch route directly
169
170The `fetch` handler is plain HTTPS against the MicroVM's endpoint (a bare
171hostname like `<id>.lambda-microvm.<region>.on.aws`). Authenticate with the
172same token via `microvmAuthHeaders`:
173
174```typescript
175const client = yield* HttpClient.HttpClient;
176const res = yield* client.get(`https://${vm.endpoint}/echo?message=hi`, {
177 headers: AWS.Lambda.microvmAuthHeaders(authToken),
178});
179const body = yield* res.json; // { message: "hi" }
180```
181
182Both surfaces coexist on one image: the RPC stub for typed calls from your own
183code, the fetch route for anything that speaks HTTP.
184
185## Wire the stack
186
187The Lambda pulls in the `Sandbox` class handle; the image's `.make()` runtime
188is provided on the Stack so it gets built and deployed alongside:
189
190```typescript
191// alchemy.run.ts
192import * as Alchemy from "alchemy";
193import * as AWS from "alchemy/AWS";
194import * as Effect from "effect/Effect";
195import Api from "./api.ts";
196import SandboxLive from "./sandbox.ts";
197
198export default Alchemy.Stack(
199 "MicrovmApp",
200 { providers: AWS.providers(), state: AWS.state() },
201 Effect.gen(function* () {
202 const api = yield* Api;
203 return { url: api.functionUrl.as<string>() };
204 }).pipe(Effect.provide(SandboxLive)),
205);
206```
207
208## Bring your own Dockerfile
209
210For non-TypeScript workloads, point `context` at a directory containing a
211Dockerfile (built `FROM public.ecr.aws/lambda/microvms:al2023-minimal`) and
212the files it copies. Alchemy zips and uploads the directory; AWS runs the
213Dockerfile server-side. There is no in-VM Effect runtime to bind, so both
214the bare build Role and the image live directly in the Stack's `Effect.gen`:
215
216```typescript
217// alchemy.run.ts
218import * as Alchemy from "alchemy";
219import * as AWS from "alchemy/AWS";
220import * as Effect from "effect/Effect";
221
222export default Alchemy.Stack(
223 "FlaskMicrovm",
224 { providers: AWS.providers(), state: AWS.state() },
225 Effect.gen(function* () {
226 const buildRole = yield* AWS.IAM.Role("ExternalBuildRole", {});
227 const image = yield* AWS.Lambda.MicrovmImage("Flask", {
228 context: `${import.meta.dirname}/app`, // dir with Dockerfile + app.py
229 buildRole,
230 });
231 return { imageArn: image.imageArn.as<string>() };
232 }),
233);
234```
235
236Re-deploys only trigger a new build when the artifact's content hash or a
237build-affecting prop changes; otherwise the image is left untouched.
238
239## Where next
240
241- [Lambda](/aws/compute/lambda) — the Function model the orchestrator is built on.
242- [MicrovmImage API reference](/providers/aws/lambda/microvmimage) — all props,
243 build modes, sizing, logging, and VPC egress via `NetworkConnector`.
244- [RunMicrovm](/providers/aws/lambda/runmicrovm) and the other per-instance
245 lifecycle bindings.