A custom Bun runtime for AWS Lambda with CDK constructs for easy deployment.
Current bun version: 1.3.14
npm i @beesolve/lambda-bun-runtimeTwo constructs are provided: BunLambdaLayer (the runtime layer) and BunFunction (a Lambda function that uses it).
import { BunLambdaLayer, BunFunction } from '@beesolve/lambda-bun-runtime';
import { Duration } from "aws-cdk-lib";
const bunLayer = new BunLambdaLayer(this, "BunLayer");
const apiHandler = new BunFunction(this, "ApiHandler", {
entrypoint: `${__dirname}/api.ts`,
memorySize: 1024,
timeout: Duration.seconds(10),
environment: {
STAGE: 'prod',
},
bunLayer,
});BunFunction accepts .ts and .js entrypoints. TypeScript files are built with Bun automatically during CDK synth. JavaScript files are used as-is.
By default, the Lambda handler string is derived as <filename>.handler. Override with the exportName prop.
Handlers use the standard Node.js-style (event, context) => response signature — the same pattern used by all official AWS Lambda runtimes:
// api.ts
export const handler = async (event: unknown, context: unknown) => {
return {
statusCode: 200,
body: JSON.stringify({ message: "Hello from Bun!" }),
};
};This is a deliberate design choice. See Why raw events? below.
Install @types/aws-lambda for typed event and result shapes:
npm i -D @types/aws-lambdaimport type { APIGatewayProxyEventV2, APIGatewayProxyResultV2 } from "aws-lambda";
export const handler = async (event: APIGatewayProxyEventV2): Promise<APIGatewayProxyResultV2> => {
const name = event.queryStringParameters?.name ?? "world";
return {
statusCode: 200,
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message: `Hello, ${name}!` }),
};
};Set-Cookie headers use the top-level cookies array in v2 responses:
return {
statusCode: 200,
cookies: ["session=abc123; Path=/; HttpOnly; SameSite=Lax"],
body: "ok",
};import type { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda";
export const handler = async (event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult> => {
const name = event.queryStringParameters?.name ?? "world";
return {
statusCode: 200,
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ message: `Hello, ${name}!` }),
};
};The same (event, context) pattern works for any Lambda trigger. Import the matching type from @types/aws-lambda:
import type { SQSEvent, SQSBatchResponse } from "aws-lambda";
export const handler = async (event: SQSEvent): Promise<SQSBatchResponse> => {
const failures = [];
for (const record of event.Records) {
// process record...
}
return { batchItemFailures: failures };
};Return a ReadableStream<Uint8Array> or any AsyncIterable<Uint8Array | string> (including async generators) to stream the response. The runtime detects the return type automatically and sends it using HTTP chunked transfer encoding with the Lambda-Runtime-Function-Response-Mode: streaming header — no wrapper or import required.
Async generator (simplest):
export async function* handler() {
yield "chunk one\n";
yield "chunk two\n";
yield "chunk three\n";
}ReadableStream:
export const handler = async () => {
const encoder = new TextEncoder();
const { readable, writable } = new TransformStream();
const writer = writable.getWriter();
(async () => {
await writer.write(encoder.encode("chunk one\n"));
await writer.write(encoder.encode("chunk two\n"));
await writer.close();
})();
return readable;
};Invoke streaming functions using the AWS SDK's InvokeWithResponseStream API, or expose them via a Function URL with invokeMode: InvokeMode.RESPONSE_STREAM.
For HTTP streaming with custom status codes and response headers (e.g., Content-Type: text/event-stream), use asResponseStreamHandler from @beesolve/lambda-fetch-api, which handles the HTTP metadata framing required by Function URLs.
This runtime passes raw Lambda events directly to your handler. It does not convert events to Fetch API Request/Response objects.
If you want to write handlers using the Fetch API — for example to share code between Bun's native HTTP server and Lambda — use the companion package @beesolve/lambda-fetch-api.
npm i @beesolve/lambda-fetch-apiThe recommended pattern exports both a fetch function (for local development with bun run --serve) and a handler function (for Lambda) from the same file:
import { asHttpV2Handler } from '@beesolve/lambda-fetch-api';
const fetch = async (request: Request): Promise<Response> => {
return new Response("Hello from Bun!");
};
export const handler = asHttpV2Handler(fetch);
export default { fetch };This dual-export pattern lets you run the same file locally and deploy it to Lambda without changes.
import { asHttpV1Handler } from '@beesolve/lambda-fetch-api';
const fetch = async (request: Request): Promise<Response> => {
const url = new URL(request.url);
return Response.json({ path: url.pathname });
};
export const handler = asHttpV1Handler(fetch);
export default { fetch };import { asResponseStreamHandler } from '@beesolve/lambda-fetch-api';
export const handler = asResponseStreamHandler(async () => {
const stream = new ReadableStream({
async start(controller) {
controller.enqueue("chunk one\n");
controller.enqueue("chunk two\n");
controller.close();
},
});
return new Response(stream, { headers: { "content-type": "text/plain" } });
});The original event and context are stored per-invocation via AsyncLocalStorage. Call the getters from anywhere inside your handler — no need to thread parameters:
import { asHttpV2Handler, getAwsV2Event, getAwsContext } from '@beesolve/lambda-fetch-api';
export const handler = asHttpV2Handler(async (request) => {
const event = getAwsV2Event(); // APIGatewayProxyEventV2
const context = getAwsContext(); // Context
console.log(event.requestContext.requestId);
console.log(context.getRemainingTimeInMillis());
return new Response("ok");
});| Getter | Returns |
|---|---|
getAwsEvent() |
APIGatewayProxyEvent | APIGatewayProxyEventV2 — auto-detected |
getAwsV1Event() |
APIGatewayProxyEvent — throws if event is v2 |
getAwsV2Event() |
APIGatewayProxyEventV2 — throws if event is v1 |
getAwsContext() |
Context |
All getters throw NotInHandlerContextError if called outside a handler invocation.
For routes protected by a Lambda authorizer, use the narrowed handler variants:
import { asLambdaAuthorizedHttpV2Handler, getAwsLambdaAuthorizerContext } from '@beesolve/lambda-fetch-api';
import * as v from 'valibot';
const AuthSchema = v.object({ userId: v.string(), role: v.string() });
export const handler = asLambdaAuthorizedHttpV2Handler(async (request) => {
const auth = await getAwsLambdaAuthorizerContext(AuthSchema);
return Response.json({ user: auth.userId });
});The v1 equivalent is asCustomAuthorizedHttpV1Handler / getAwsCustomAuthorizerContext. Any Standard Schema-compatible library (Zod, Valibot, ArkType) works for validating the payload.
import { getAwsEvent, isAPIGatewayProxyEventV2 } from '@beesolve/lambda-fetch-api';
const event = getAwsEvent();
if (isAPIGatewayProxyEventV2(event)) {
console.log(event.rawPath); // APIGatewayProxyEventV2
} else {
console.log(event.path); // APIGatewayProxyEvent
}WebSocket handling is not supported out of the box. Lambda functions are request-response by nature and don't maintain persistent connections.
If you need WebSocket support, consider using API Gateway WebSocket APIs with separate $connect, $disconnect, and $default route handlers — each deployed as a standard BunFunction with the raw event signature.
If you're migrating from the previous Fetch API-based runtime (or from the official bun-lambda package), here's what changed and how to adapt.
The previous runtime converted Lambda events into Fetch API Request objects and expected a Response back. The new runtime passes raw (event, context) directly — no conversion layer.
Replace your Fetch-style handler with a standard Lambda handler:
Before (Fetch API style):
export default {
fetch: async (request: Request): Promise<Response> => {
const url = new URL(request.url);
return new Response(`Path: ${url.pathname}`);
},
};After (raw event style):
import type { APIGatewayProxyEventV2 } from "aws-lambda";
export const handler = async (event: APIGatewayProxyEventV2) => {
return {
statusCode: 200,
body: `Path: ${event.rawPath}`,
};
};If you have existing Fetch-based handlers and want to keep them, install the companion package and wrap them:
npm i @beesolve/lambda-fetch-apiBefore (worked with old runtime directly):
export default {
fetch: async (request: Request): Promise<Response> => {
return new Response("Hello!");
},
};After (works with new runtime via adapter):
import { asHttpV2Handler } from '@beesolve/lambda-fetch-api';
const fetch = async (request: Request): Promise<Response> => {
return new Response("Hello!");
};
export const handler = asHttpV2Handler(fetch);
// Keep for local development with `bun run --serve`
export default { fetch };Use asHttpV1Handler instead if your API is backed by API Gateway v1 (REST API).
| Aspect | Old runtime | New runtime |
|---|---|---|
| Handler signature | fetch(request: Request): Response |
handler(event, context): any |
| Event format | Converted to Fetch Request |
Raw Lambda event (e.g., APIGatewayProxyEventV2) |
| Response format | Fetch Response |
Lambda response object (e.g., { statusCode, body }) |
| Fetch API support | Built-in | Via @beesolve/lambda-fetch-api adapter |
| WebSocket support | Built-in (via Bun.serve) | Not applicable (use API Gateway WebSocket APIs) |
| Local development | bun run --serve with same file |
Same file works with dual-export pattern |
The previous approach (converting Lambda events to Fetch API objects) introduced complexity and overhead:
- Conversion cost — every invocation paid the price of constructing a
Requestand parsing aResponse, even for non-HTTP triggers (SQS, S3, EventBridge, etc.) - Lossy abstraction — Lambda events contain metadata (request context, authorizer claims, stage variables) that doesn't map cleanly to HTTP headers
- Trigger lock-in — Fetch API only makes sense for HTTP triggers, but Lambda functions handle many event sources
- Debugging friction — when something goes wrong, you're debugging two layers: the event-to-Request conversion and your actual logic
The raw event approach means:
- Zero overhead — events pass through untouched
- Works with any Lambda trigger (HTTP, SQS, S3, EventBridge, DynamoDB Streams, etc.)
- Standard Lambda patterns — all AWS documentation and examples apply directly
- Fetch API is opt-in via
@beesolve/lambda-fetch-apifor those who want the dual-environment pattern