Skip to content

Function

Source: src/AWS/Lambda/Function.ts

An AWS Lambda host resource that combines code bundling, IAM role provisioning, and runtime binding collection.

Function is the canonical runtime host for AWS. Alchemy automatically bundles your TypeScript entry module with Rolldown, creates an IAM execution role, and uploads the zip artifact. On subsequent deploys, the function is only updated when the bundle hash changes.

There are two ways to define a Lambda Function:

  • Async — plain handler export, no Effect runtime in the bundle.
  • Effect — Effect implementation with typed bindings and event sources.

See Effect handlers vs async handlers for plain handler patterns, or the Lambda guide for the full Effect-based approach with bindings, event sources, and sinks.

Point main at a file that exports a standard Lambda handler. No Effect runtime is included in the bundle. Useful when migrating existing Lambda functions or when you don’t need Effect.

Defining an async Lambda in your stack

alchemy.run.ts
import * as AWS from "alchemy/AWS";
const func = yield* AWS.Lambda.Function("ApiFunction", {
main: "./src/handler.ts",
url: true,
});

Function using ARM64

const func = yield* AWS.Lambda.Function("ArmFunction", {
main: "./src/handler.ts",
architecture: "arm64",
});

Function with a native package (Sharp)

const func = yield* AWS.Lambda.Function("ImageProcessor", {
main: "./src/handler.ts",
architecture: "arm64",
build: {
install: ["sharp"],
},
});

Writing the async handler

src/handler.ts
export const handler = async (event: any) => {
return {
statusCode: 200,
body: JSON.stringify({ message: "Hello from Lambda!" }),
};
};

Pass the Effect implementation as the third argument. Bindings attach IAM permissions and environment variables at deploy time, while the runtime execution context collects listeners and exports.

export default class ApiFunction extends AWS.Lambda.Function<ApiFunction>()(
"ApiFunction",
{ main: import.meta.url, url: true },
Effect.gen(function* () {
// init: bind resources
const getItem = yield* AWS.DynamoDB.GetItem(table);
return {
// runtime: use them
fetch: Effect.gen(function* () {
const request = yield* HttpServerRequest;
const url = new URL(request.url);
const id = url.searchParams.get("id");
const result = yield* getItem({ Key: { pk: { S: id! } } });
return yield* HttpServerResponse.json(result.Item);
}),
};
}),
) {}

Function with URL

const func = yield* AWS.Lambda.Function("ApiFunction", {
main: "./src/handler.ts",
url: true,
});

Function URL with IAM auth

const func = yield* AWS.Lambda.Function("ApiFunction", {
main: "./src/handler.ts",
url: {
authType: "AWS_IAM",
},
});

Function in a VPC

const func = yield* AWS.Lambda.Function("VpcFunction", {
main: "./src/handler.ts",
vpc: {
subnetIds: ["subnet-abc123", "subnet-def456"],
securityGroupIds: ["sg-xyz789"],
},
});

Async invocation retries and failure destination

const func = yield* AWS.Lambda.Function("AsyncFunction", {
main: "./src/handler.ts",
eventInvokeConfig: {
maximumRetryAttempts: 0,
maximumEventAge: "1 minute",
destinationConfig: {
OnFailure: {
Destination: queue.queueArn,
},
},
},
});

main is bundled with rolldown at deploy time. Top-level calls in the effect, @effect/*, alchemy, @alchemy.run/*, and @distilled.cloud/* packages receive #__PURE__ annotations by default, so anything the function doesn’t use from those packages is tree-shaken out of the bundle. Any other package — including your own app — is left untouched unless you list it explicitly.

Treat additional packages as pure

Pass package names (or picomatch globs) via build.pure.packages to annotate them in addition to the defaults. Listing a package that also declares "sideEffects": false (or []) in its package.json opts it into full annotation — top-level calls whose result is discarded are deleted under minification when unused — so only list packages whose modules really are free of meaningful top-level side effects.

const func = yield* AWS.Lambda.Function("ApiFunction", {
main: "./src/handler.ts",
build: {
pure: { packages: ["my-lib", "@my-scope/*"] },
},
});

Disable pure annotations

const func = yield* AWS.Lambda.Function("ApiFunction", {
main: "./src/handler.ts",
build: { pure: false },
});

Mount an EFS access point into the function’s /mnt/… file system. The function must be attached to a VPC that can reach an EFS mount target for the file system.

Mount an EFS access point via props

const accessPoint = yield* AWS.EFS.AccessPoint("FilesAccess", {
fileSystemId: fileSystem.fileSystemId,
posixUser: { uid: 1000, gid: 1000 },
});
const func = yield* AWS.Lambda.Function("FilesFunction", {
main: "./src/handler.ts",
vpc: { subnetIds, securityGroupIds },
fileSystemConfigs: [
// pass the AccessPoint resource itself (or its ARN via `arn`)
{ accessPoint, localMountPath: "/mnt/files" },
],
});

Mount via the host-agnostic EFS.mount binding

EFS.mount wires the same mount config plus least-privilege IAM through the binding channel and works on both Lambda and ECS hosts.

export default class FilesFunction extends AWS.Lambda.Function<FilesFunction>()(
"FilesFunction",
{ main: import.meta.url, vpc: { subnetIds, securityGroupIds } },
Effect.gen(function* () {
const files = yield* AWS.EFS.mount(accessPoint, { path: "/mnt/files" });
return Effect.fn(function* (event: unknown) {
return { mountedAt: files.path };
});
}).pipe(Effect.provide(AWS.EFS.MountLive)),
) {}

Bind S3 operations in the init phase to give the function IAM permissions and inject the bucket name as an environment variable.

// init
const getObject = yield* S3.GetObject(bucket);
const putObject = yield* S3.PutObject(bucket);
return {
fetch: Effect.gen(function* () {
// runtime
yield* putObject({ Key: "hello.txt", Body: "Hello!" });
const obj = yield* getObject({ Key: "hello.txt" });
return HttpServerResponse.text("OK");
}),
};

Bind DynamoDB operations in the init phase to grant table-scoped IAM permissions.

// init
const getItem = yield* AWS.DynamoDB.GetItem(table);
const putItem = yield* AWS.DynamoDB.PutItem(table);
return {
fetch: Effect.gen(function* () {
// runtime
yield* putItem({ Item: { pk: { S: "user#1" }, name: { S: "Alice" } } });
const result = yield* getItem({ Key: { pk: { S: "user#1" } } });
return yield* HttpServerResponse.json(result.Item);
}),
};

Bind SQS operations in the init phase to send messages to a queue.

// init
const sendMessage = yield* SQS.SendMessage(queue);
return {
fetch: Effect.gen(function* () {
// runtime
yield* sendMessage({
MessageBody: JSON.stringify({ orderId: "123" }),
});
return HttpServerResponse.text("Queued");
}),
};

Bind SNS operations in the init phase to publish messages to a topic.

// init
const publish = yield* AWS.SNS.Publish(topic);
return {
fetch: Effect.gen(function* () {
// runtime
yield* publish({
Message: JSON.stringify({ event: "order.created" }),
Subject: "OrderCreated",
});
return HttpServerResponse.text("Published");
}),
};

Bind Kinesis operations in the init phase to put records into a stream.

// init
const putRecord = yield* AWS.Kinesis.PutRecord(stream);
return {
fetch: Effect.gen(function* () {
// runtime
yield* putRecord({
PartitionKey: "order-123",
Data: new TextEncoder().encode(JSON.stringify({ orderId: "123" })),
});
return HttpServerResponse.text("Sent");
}),
};

Lambda functions can be triggered by event sources like SQS queues, DynamoDB streams, S3 notifications, SNS topics, and Kinesis streams.

Process SQS messages

yield* SQS.consumeQueueMessages(queue,
Effect.fn(function* (message) {
yield* Effect.log(`Received: ${message.body}`);
}),
);

Process DynamoDB stream changes

yield* AWS.DynamoDB.consumeTableChanges(table, {
StreamViewType: "NEW_AND_OLD_IMAGES",
},
Effect.fn(function* (record) {
yield* Effect.log(`Change: ${record.eventName}`);
}),
);

Process S3 notifications

yield* AWS.S3.consumeBucketEvents(bucket, {
events: ["s3:ObjectCreated:*"],
}, (stream) =>
stream.pipe(
Stream.runForEach((event) =>
Effect.log(`New object: ${event.key}`),
),
),
);