1---
2title: Lambda
3description: Stand up an AWS Lambda Function from a single Effect, expose it over a Function URL, and call it from a test.
4---
5
6**Lambda** is Alchemy's default AWS runtime: a class that bundles
7an Effect program into a zip, deploys it as a Lambda Function,
8and generates its IAM execution role from the bindings you
9actually use. Serve HTTP over a public **Function URL**, or
10consume events — every building block in this section plugs in
11through the same pattern: S3 notifications
12(`Lambda.BucketEventSource`), SQS queues
13(`Lambda.QueueEventSource`), Kinesis streams
14(`Lambda.StreamEventSource`), and DynamoDB Streams
15(`Lambda.TableEventSource`).
16
17This page stands up the smallest possible function — a public
18HTTP endpoint — which the [S3](/aws/data/s3),
19[DynamoDB](/aws/data/dynamodb), [SQS](/aws/messaging/sqs), and
20[Kinesis](/aws/messaging/kinesis) pages then grow into a small
21event-driven system.
22
23## Prerequisites
24
25Install Alchemy and connect your AWS account — see
26[Setup](/aws/setup).
27
28## Create the Stack
29
30Every Alchemy app has an `alchemy.run.ts` at the root that
31declares the resources to deploy. Create one with the AWS
32providers wired in:
33
34```typescript
35// alchemy.run.ts
36import * as Alchemy from "alchemy";
37import * as AWS from "alchemy/AWS";
38import * as Effect from "effect/Effect";
39
40export default Alchemy.Stack(
41 "MyApp",
42 {
43 providers: AWS.providers(),
44 state: AWS.state(),
45 },
46 Effect.gen(function* () {
47 return {};
48 }),
49);
50```
51
52`Alchemy.Stack` is the root of every app. The first argument
53(`"MyApp"`) doubles as a logical id and as the prefix Alchemy
54uses when AWS asks for physical resource names.
55
56`providers: AWS.providers()` registers every AWS resource and
57IAM policy binding that ships with Alchemy, and resolves
58credentials from your Alchemy profile — SSO, environment
59variables, or stored keys, whichever you picked in
60[Setup](/aws/setup).
61
62`state: AWS.state()` stores deploy state in an account-regional
63S3 bucket (`alchemy-state-{accountId}-{region}-an`), created
64lazily on the first deploy. Because the state lives in your AWS
65account rather than on disk, the same configuration works
66locally and in CI. (For purely local iteration you can use
67`Alchemy.localState()` instead, which writes state under
68`.alchemy/` next to your code.)
69
70The trailing `Effect.gen` block is where you'll declare the
71resources to deploy. It's empty for now.
72
73## Declare a Lambda Function
74
75A Lambda Function in Alchemy is a class. Create `src/api.ts`
76with the smallest possible declaration — just the class
77ceremony, an entrypoint, and an empty runtime:
78
79```typescript
80// src/api.ts
81import * as AWS from "alchemy/AWS";
82import * as Effect from "effect/Effect";
83
84export default class Api extends AWS.Lambda.Function<Api>()(
85 "Api",
86 { main: import.meta.url },
87 Effect.gen(function* () {
88 return {};
89 }),
90) {}
91```
92
93The `<Api>` type argument plus the empty `()` is a one-time bit
94of ceremony — it lets TypeScript reason about `Api` as a typed
95handle that other resources can later bind against. The rest of
96your code looks completely normal.
97
98`main: import.meta.url` tells Alchemy this same file is the
99bundle entrypoint. At deploy time it'll be bundled with Rolldown
100into a zip and uploaded as the function's code.
101
102## Serve HTTP from a Function URL
103
104The empty function compiles, but it doesn't do anything yet.
105Add a `fetch` field — Alchemy treats anything returned from the
106`Effect.gen` block as the runtime API, and `fetch` specifically
107is wired up to handle incoming HTTP requests:
108
109```diff lang="typescript"
110// src/api.ts
111import * as AWS from "alchemy/AWS";
112import * as Effect from "effect/Effect";
113+import * as HttpServerResponse from "effect/http/HttpServerResponse";
114
115export default class Api extends AWS.Lambda.Function<Api>()(
116 "Api",
117 { main: import.meta.url },
118 Effect.gen(function* () {
119- return {};
120+ return {
121+ fetch: Effect.succeed(HttpServerResponse.text("Hello from Lambda!")),
122+ };
123 }),
124) {}
125```
126
127`HttpServerResponse.text(...)` is the same `effect/http`
128API used everywhere else — Alchemy adapts it to the Lambda event
129envelope under the hood, so your handler never sees the raw
130`APIGatewayProxyEvent` shape.
131
132## Expose a public URL
133
134`fetch` exists, but no one can call it yet. Set `functionUrl: true` on
135the props to ask AWS for a public **Function URL** — no API
136Gateway, no auth, just a public HTTPS endpoint:
137
138```diff lang="typescript"
139export default class Api extends AWS.Lambda.Function<Api>()(
140 "Api",
141- { main: import.meta.url },
142+ { main: import.meta.url, functionUrl: true },
143 Effect.gen(function* () {
144 return {
145 fetch: Effect.succeed(HttpServerResponse.text("Hello from Lambda!")),
146 };
147 }),
148) {}
149```
150
151The resolved `Api` resource will now expose a `functionUrl`
152field carrying that endpoint — we'll surface it from the Stack
153in a moment.
154
155## Customize per stage
156
157Lambda has knobs you'll want to tune per stage — memory,
158timeout, log retention. Swap the static props object for
159`Stack.useSync`, which gives you a synchronous accessor for any
160value in the surrounding Effect context:
161
162```diff lang="typescript"
163// src/api.ts
164import * as AWS from "alchemy/AWS";
165+import { Stack } from "alchemy/Stack";
166import * as Effect from "effect/Effect";
167import * as HttpServerResponse from "effect/http/HttpServerResponse";
168
169export default class Api extends AWS.Lambda.Function<Api>()(
170 "Api",
171- { main: import.meta.url, functionUrl: true },
172+ Stack.useSync((stack) => ({
173+ main: import.meta.url,
174+ functionUrl: true,
175+ memory: stack.stage === "prod" ? 1024 : 512,
176+ })),
177 Effect.gen(function* () {
178 return {
179 fetch: Effect.succeed(HttpServerResponse.text("Hello from Lambda!")),
180 };
181 }),
182) {}
183```
184
185`Stack.useSync` is the synchronous accessor for any value in the
186surrounding Effect context — handy for stack-level config like
187`stage`, `appName`, or anything else you'd want to vary per
188environment.
189
190## Wire the function into the Stack
191
192The `Api` class is just a typed identifier — yielding it inside
193the Stack's Effect is what registers the resource and starts the
194deploy:
195
196```diff lang="typescript"
197// alchemy.run.ts
198import * as Alchemy from "alchemy";
199import * as AWS from "alchemy/AWS";
200import * as Effect from "effect/Effect";
201+import Api from "./src/api.ts";
202
203export default Alchemy.Stack(
204 "MyApp",
205 {
206 providers: AWS.providers(),
207 state: AWS.state(),
208 },
209 Effect.gen(function* () {
210+ const api = yield* Api;
211+ return { url: api.functionUrl };
212- return {};
213 }),
214);
215```
216
217Yielding `Api` returns the resolved Lambda outputs — the function
218ARN, role ARN, log group, and the public Function URL we asked
219for with `functionUrl: true`. We surface `functionUrl` as the Stack's
220`url` so the test harness can find it.
221
222## Deploy
223
224```sh
225bun alchemy deploy
226```
227
228Alchemy bundles `src/api.ts` with Rolldown, packages it into a
229zip, creates the IAM execution role, uploads the function, and
230provisions the Function URL. The first deploy takes a moment
231because of role propagation; subsequent deploys are seconds.
232
233## Verify
234
235Drop a quick integration test that hits the Function URL and
236checks the body:
237
238```typescript
239// test/integ.test.ts
240import * as AWS from "alchemy/AWS";
241import * as Test from "alchemy/Test/Bun";
242import { expect } from "bun:test";
243import * as Effect from "effect/Effect";
244import * as HttpClient from "effect/http/HttpClient";
245import Stack from "../alchemy.run.ts";
246
247const { test, beforeAll, deploy } = Test.make({
248 providers: AWS.providers(),
249 state: AWS.state(),
250});
251
252const stack = beforeAll(deploy(Stack));
253
254test(
255 "Api responds over Function URL",
256 Effect.gen(function* () {
257 const { url } = yield* stack;
258 const response = yield* HttpClient.get(url);
259 expect(yield* response.text).toBe("Hello from Lambda!");
260 }),
261);
262```
263
264```sh
265bun test test/integ.test.ts
266```
267
268You now have a deployable Lambda with a public URL.
269
270## Sandbox scope vs invocation scope
271
272The constructor runs **once per sandbox**, at cold start —
273everything it builds (bindings, SDK clients) is reused by every
274invocation. Each invocation then runs with a **fresh `Scope`** that
275is settled *inline* before the handler returns — so
276`Effect.addFinalizer` in a handler runs before the response leaves,
277and request finalizers should be fast (closing a pool is
278milliseconds).
279
280:::caution[There is no `waitUntil` on Lambda — request finalizers block the response]
281No mechanism delivers post-response work reliably on Lambda. A
282buffered invocation's response is not released to the caller until
283the entire Invoke phase completes, so deferring cleanup past the
284handler (we measured an extension-held post-response window) still
285shows up as response latency — and environments whose invoke phase
286is held open this way get recycled. Dangling promises resume on a
287later thaw at best: TCP connections rarely survive the freeze, and a
288crash or timeout reset drops the work entirely — silent data loss
289for anything that mattered.
290
291The contract is blocking and honest: request finalizers run before
292the response, keep them cheap, and anything that must not be lost
293gets written durably (a queue, a table) inside the handler itself.
294:::
295
296Construction-level finalizers run at **sandbox shutdown**. A Lambda sandbox
297with no registered extensions is killed with no signal at all, so
298the generated entry registers an internal extension with the
299Extensions API — that makes Lambda send `SIGTERM` and allow 500 ms
300before `SIGKILL`, and the entry uses the window to close the
301instance scope:
302
303```typescript
304Effect.gen(function* () {
305 // Construction: runs once per sandbox
306 yield* Effect.addFinalizer(() =>
307 // runs in the 500 ms SIGTERM window at sandbox spin-down
308 flushTelemetry().pipe(Effect.ignore),
309 );
310 return { fetch: /* per-invocation */ };
311});
312```
313
314Treat instance-level cleanup as best-effort: half a second, and not
315delivered on hard failures (a timeout reset kills the runtime
316without the signal). Flush caches and close connections there;
317anything that *must* happen belongs in the handler, scoped to the
318invocation. See
319[Instance scope vs request scope](/infrastructure-as-effects/runtime#instance-scope-vs-request-scope)
320for the model across all runtimes.
321
322## Where next
323
324- [Drizzle + Aurora PostgreSQL](/aws/data/drizzle-aurora) — private database access,
325 IAM authentication, verified TLS, and deployment-time schema setup.
326- [Drizzle + Aurora DSQL](/aws/data/drizzle-dsql) — the separate serverless database,
327 with an IAM-authenticated endpoint and non-admin application role.
328
329- [S3](/aws/data/s3) — add a bucket and bind read/write operations
330 into this function; IAM policies generated from the call
331 sites, no policy JSON to hand-write.
332- [Secrets & env](/aws/security/secrets-env) — API keys from .env and
333 Secrets Manager credentials for this function.
334- [REST API (API Gateway v1)](/aws/apis/api-gateway) — put an
335 API Gateway REST API with stages and custom domains in front
336 instead of a Function URL.
337- [Choosing a runtime](/aws/compute/choosing-a-runtime) — when ECS,
338 EKS, or EC2 fits better than Lambda.
339- [`Function` reference](/providers/aws/lambda/function) — every
340 prop and attribute.