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. It can either bundle a TypeScript entry module into a zip artifact or build a user-authored Dockerfile into a Lambda container image. In both modes Alchemy creates the execution role and applies bindings; image mode additionally owns the private ECR repository and Lambda pull policy.

Zip-packaged functions can be defined in two ways:

  • 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",
functionUrl: 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!" }),
};
};

Set image instead of main to deploy an existing private ECR image or to build and publish a local Docker context. Image sources must be literal because Lambda’s pre-create phase needs the deployable image before normal Output resolution.

const func = yield* AWS.Lambda.Function("Worker", {
image: {
uri: "123456789012.dkr.ecr.us-east-1.amazonaws.com/worker@sha256:...",
command: ["app.handler"],
entryPoint: ["/lambda-entrypoint.sh"],
workingDirectory: "/var/task",
},
architecture: "x86_64",
});

Tagged URIs are resolved through ECR on each plan. If a tag is repointed to a new digest, Alchemy updates the Lambda function even though the URI string is unchanged. External repositories are never modified or deleted.

const func = yield* AWS.Lambda.Function("JavaFunction", {
image: {
context: "./lambda",
dockerfile: "Dockerfile",
buildArgs: {
APP_ENV: "production",
},
},
architecture: "arm64",
functionUrl: false,
});

The Dockerfile owns the runtime and handler. Alchemy does not generate a Node.js adapter or otherwise impose a language. For example, ./lambda/Dockerfile can use AWS’s Java base image:

FROM public.ecr.aws/lambda/java:21
COPY target/function.jar ${LAMBDA_TASK_ROOT}/lib/
CMD ["com.example.Handler::handleRequest"]

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, functionUrl: 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",
functionUrl: true,
});

Function URL with IAM auth

const func = yield* AWS.Lambda.Function("ApiFunction", {
main: "./src/handler.ts",
functionUrl: {
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}`),
),
),
);