Awesome
AWS Lambda Runtime for Zig
Write AWS Lambda functions in the Zig programming language to achieve blazing fast invocations and cold starts!
🐣 Quick Start · 📒 Documentation · 💽 Demos
Features
- Runtime API
- Extensions API
- Telemetry API
- Response streaming
- CloudWatch & X-Ray integration
- Lifecycle hooks
- Dependency injection
- Build system target configuration
- Managed build step
- Testing utilities
Services Events
Feel free to open an issue for additional integrations, or better contribute a pull request.
- Unified HTTP
- Lambda URLs
- API Gateway
- S3
- SQS
- SNS
- DynamoDB
- Data Firehose
Benchmark
Using zig allows creating small and fast functions.<br /> Minimal Hello World demo (arm64, 256 MiB, Amazon Linux 2023):
- ❄️
~11ms
cold start invocation duration - ⚡
~1.5ms
warm invocation duration - 💾
12 MiB
max memory consumption - ⚖️
0.36 MiB
function size (zip)
[!TIP] Check out AWS SDK for Zig for a comprehensive Zig-based AWS cloud solution.
Quick Start
- Add a dependency to your project (replace
VERSION
with the desired version tag):zig fetch --save git+https://github.com/by-nir/aws-lambda-zig#VERSION
- Configure the build script.
- Implement a handler function (either an event handler or a streaming handler).
- Build a handler executable for the preferred target architecture:
zig build --release -Darch=x86 zig build --release -Darch=arm
- Archive the executable into a zip:
zip -qj lambda.zip zig-out/bin/bootstrap
- Deploy the zip archive to a Lambda function:
- Configure it with Amazon Linux 2023 or other OS-only runtime.
- Use you prefered deployment method: console, CLI, SAM or any CI solution.
Build Script
const std = @import("std");
const lambda = @import("aws-lambda");
pub fn build(b: *std.Build) void {
const optimize = b.standardOptimizeOption(.{
.preferred_optimize_mode = .ReleaseFast,
});
// Add an architecture confuration option and resolves a target query
const target = lambda.resolveTargetQuery(b, lambda.archOption(b));
// Add the handler executable
const exe = b.addExecutable(.{
.name = "bootstrap", // The executable name must be "bootstrap"!
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
// .link_libc = true, // Uncomment if glibc is required.
// .strip = true, // Uncomment if no stack traces are needed.
});
b.installArtifact(exe);
// Import the runtime module
const runtime = b.dependency("aws-lambda", .{}).module("lambda");
exe.root_module.addImport("aws-lambda", runtime);
}
Event Handler
const lambda = @import("aws-lambda");
// Entry point for the Lambda function.
pub fn main() void {
// Bind the handler to the runtime:
lambda.handle(handler, .{});
}
// Eeach event is processed separetly the handler function.
// The function must have the following signature:
fn handler(
ctx: lambda.Context, // Metadata and utilities
event: []const u8, // Raw event payload (JSON)
) ![]const u8 {
return "Hello, world!";
}
Documentation
Build
This library provides a runtime module that handles the Lambda lifecycle and communication with the execution environment. To use it, follow the following requirements: - Import the Lambda Runtime module this library provides and wrap a handler function with it. - Build an executable named bootstrap and archive it in a Zip file. - Use Amazon Linux 2023 runtime.
Managed Target
AWS Lambda supports two architectures: x86_64 and arm64 based on Graviton2. In order to build the event handler correctly and to squeeze the best performance, the build target must be configured accordingly.
The mananged target resolver sets the optimal operating system, architecture and specific CPU supported features. Call lambda.resolveTargetQuery(*std.Build, arch)
to resolve the target for the given architecture (either .x86
or .arm
).
To add a CLI configuration option call lambda.archOption(*std.Build)
the following and pass the result to lambda.resolveTargetQuery
.
It can then by set through -Darch=x86
or -Darch=arm
(defaults to x86 when to manualy used).
Example Build Script
const std = @import("std");
const lambda = @import("aws-lambda");
pub fn build(b: *std.Build) void {
const optimize = b.standardOptimizeOption(.{
.preferred_optimize_mode = .ReleaseFast,
});
// Add an architecture CLI option (or hard code either `.x86` or `.arm`)
const arch: lambda.Arch = lambda.archOption(b);
// Managed architecture target resolver
const target = lambda.resolveTargetQuery(b, arch);
// Add the handler’s executable
const exe = b.addExecutable(.{
.name = "bootstrap", // The executable name must be "bootstrap"!
.root_source_file = b.path("src/main.zig"),
.target = target,
.optimize = optimize,
// .link_libc = true, // Uncomment if glibc is required.
// .strip = true, // Uncomment if no stack traces are needed.
});
b.installArtifact(exe);
// Import the runtime module
const runtime = b.dependency("aws-lambda", .{}).module("lambda");
exe.root_module.addImport("aws-lambda", runtime);
}
Event Handler
The event handler is the entry point for the Lambda function.
The library provides a runtime that handles the event lifecycle and communication with the Lambda’s execution environment. With it, you can focus on imlementing only the meaningful part of processing and responding to the event.
Since the library manages the lifecycle, it expects the handler to have a specific signature. Note that response streaming has a dedicated lifecycle and handler signature.
const lambda = @import("aws-lambda");
// Entry point for the Lambda function.
// Eeach event is processed separetly the handler function.
pub fn main() void {
// Bind the handler to the runtime:
lambda.handle(handlerSync, .{});
// Alternatively, for asynchronous handlers:
lambda.handleAsync(handlerAsync, .{});
}
fn handlerSync(
ctx: lambda.Context, // Metadata and utilities
event: []const u8, // Raw event payload (JSON)
) ![]const u8 {
// Process the `event` payload and return a response payload.
return switch(payload.len) {
0 => "Empty payload.",
else => event,
};
}
fn handlerAsync(
ctx: lambda.Context, // Metadata and utilities
event: []const u8, // Raw event payload (JSON)
) !void {
// Process the `event` payload...
}
Errors & Logging
When a handler returns an error, the runtime will log it to CloudWatch and return an error response to the client.
The runtime exposes a static logging function that can be used to manually log messages to CloudWatch. The function follows Zig’s standard logging conventions.
const lambda = @import("aws-lambda");
lambda.log.err("This error is logged to {s}.", .{"CloudWatch"});
[!WARNING] In release mode only error level is preserved, other levels are removed at compile time. This behavior may be overriden at build.
Handler Context
The handler signature includes the parameter ctx: lambda.Context
, it provides metadata and utilities to assist with the processing the event.
The following sections describe the context...
Memory Allocation
Since the runtime manages the function and invocation lifecycle, it also owns the memory. The handler context provides two allocators:
Allocator | Behavior |
---|---|
ctx.gpa | You own the memory and must deallocate it by the end of the invocation. |
ctx.arena | The memory is tied to the invocation’s lifetime. The runtime will deallocate it on your behalf after the invocation resolves. |
[!NOTE] While
ctx.gpa
may be used to persist data and services between invocations, for such cases consider using dependency injection or extensions.
Environment Variables
The functions’ environment variables are mapped by the handler context, they can be accesed using the env(key)
method:
const foo_value = ctx.env("FOO_KEY") orelse "some_default_value";
Invocation Metadata
Per-invocation metadata is provided by the handler context ctx.request
field. It contains the following fields:
Field | Type | Description |
---|---|---|
request_id | []const u8 | AWS request ID associated with the request. |
xray_trace | []const u8 | X-Ray tracing id. |
invoked_arn | []const u8 | The function ARN requested. It may be different in each invoke that executes the same version. |
deadline_ms | u64 | Function execution deadline counted in milliseconds since the Unix epoch. |
client_context | []const u8 | Information about the client application and device when invoked through the AWS Mobile SDK. |
cognito_identity | []const u8 | Information about the Amazon Cognito identity provider when invoked through the AWS Mobile SDK. |
Configuration Metadata
Static config metadata is provided by the handler context ctx.config
field. It contains the following fields:
Field | Type | Description |
---|---|---|
aws_region | []const u8 | AWS Region where the Lambda function is executed. |
aws_access_id | []const u8 | Access key obtained from the function's execution role. |
aws_access_secret | []const u8 | Access key obtained from the function's execution role. |
aws_session_token | []const u8 | Access key obtained from the function's execution role. |
func_name | []const u8 | Name of the function. |
func_version | []const u8 | Version of the function being executed. |
func_size | u16 | Amount of memory available to the function in MB. |
func_init | InitType | Initialization type of the function. |
func_handler | []const u8 | Handler location configured on the function. |
log_group | []const u8 | Name of the Amazon CloudWatch Logs group for the function. |
log_stream | []const u8 | Name of the Amazon CloudWatch Logs stream for the function. |
Force Termination
Request the Lambda execution crash the runtime AFTER returning the response to the client.
ctx.forceTerminateAfterResponse();
[!WARNING] Use with caution! Only use this method when you assume the function won’t behave as expected in the following invocation.
Response Streaming
The runtime supports streaming responses to the client; though implementing a streaming handler differs from the standard handler.
const lambda = @import("aws-lambda");
// Entry point for the Lambda function.
pub fn main() void {
// Bind the handler to the runtime:
lambda.handleStream(handler, .{});
}
// Eeach event is processed separetly the handler function.
// The function must have the following signature:
fn handler(
ctx: lambda.Context, // Metadata and utilities
event: []const u8, // Raw event payload (JSON)
stream: lambda.Stream, // Stream delegate
) !void {
// Start streaming the response for a given content type.
try stream.open("text/event-stream");
// Append to the streaming buffer.
try stream.write("data: Message");
try stream.writeFmt(" number {d}\n\n", .{1});
// Publish the buffer to the client.
try stream.flush();
// Wait for half a second.
std.time.sleep(500_000_000);
// Append to streaming buffer and immediatly publish to the client.
try stream.publish("data: Message number 2\n\n");
std.time.sleep(100_000_000);
// Publish also supports formatting.
try stream.publishFmt("data: Message number {d}\n\n", .{3});
// Optionally close the stream.
try stream.close();
}
Stream Delegate
Instead of resolving payload by returning from the handler we use the stream delegate.
Once a content type is known, the handler should call stream.open(content_type)
to open the response stream. After the stream is opened you may incrementally append to the response.
Closing the stream is not required.
Method | Description |
---|---|
stream.open(content_type) | Opens the response stream for a provided HTTP content type. |
stream.openWith(content_type, raw_http_format, args) | Opens the response stream for a provided HTTP content type and initial body payload. The user MUST format the payload with proper HTTP semantics (or use a Event Encoder). |
stream.writer() | Writer for appending to the response buffer. |
stream.write(message) | Appends a message to the response buffer. |
stream.flush() | Publish the response buffer to the client. |
stream.publish(message) | Appends a message to the buffer and immediatly publish it to the client. |
stream.close() | Optionally conclude the response stream while continuing to process the event. |
Lifecycle Hooks
Not yet implemented.
Dependency Injection
Not yet implemented.
Extensions
Not yet implemented.
Demos
Hello World
Returns a short message.
zig build demo:hello --release -Darch=ARCH_OPTION
Debug
🛑 Deploy with caution! May expose sensitive data to the public.
Returns the raw payload as-is:
zig build demo:echo --release -Darch=ARCH_OPTION
Returns the function’s metadata, environment variables and the event’s raw payload:
zig build demo:debug --release -Darch=ARCH_OPTION
Errors
Immediatly returns an error; the runtime logs the error to CloudWatch:
zig build demo:fail --release -Darch=ARCH_OPTION
Returns an output larger than the Lambda limit; the runtime logs an error to CloudWatch:
zig build demo:oversize --release -Darch=ARCH_OPTION
Force the Lambda function instance the terminate after returning a response:
zig build demo:terminate --release -Darch=ARCH_OPTION
🛑 Use with caution! Only use this method when you assume the function won’t behave as expected in the following invocation.
Response Streaming
👉 Be sure to configure the Lambda function with URL enabled and RESPONSE_STREAM invoke mode.
Stream a response to the client and continue execution of the response conclusion:
zig build demo:stream --release -Darch=ARCH_OPTION
Lambda URLs
Use Lambda URLs buffered invoke to serve dynamic web pages:
zig build demo:url --release -Darch=ARCH_OPTION
Use Lambda URLs response streaming to serve dynamic updates:
zig build demo:url_stream --release -Darch=ARCH_OPTION
License
The author and contributors are not responsible for any issues or damages caused by the use of this software, part of it, or its derivatives. See LICENSE for the complete terms of use.
AWS Lambda Runtime for Zig is not an official Amazon Web Services software, nor is it affiliated with Amazon Web Services, Inc.