A custom Bun runtime for AWS Lambda with CDK constructs for easy deployment.
Note
v3 is ESM-only and no longer published as a jsii package. Polyglot CDK support (Python, Java, .NET) has been removed — there were zero consumers. If you need the layer without CDK, the zip is available as a GitHub Release asset. See ADR: Drop jsii for the full rationale.
Current bun version: 1.4.0
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";constbunLayer=newBunLambdaLayer(this,"BunLayer");constapiHandler=newBunFunction(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.tsexportconsthandler=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-lambdaimporttype{APIGatewayProxyEventV2,APIGatewayProxyResultV2}from"aws-lambda";exportconsthandler=async(event: APIGatewayProxyEventV2): Promise<APIGatewayProxyResultV2>=>{constname=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",};importtype{APIGatewayProxyEvent,APIGatewayProxyResult}from"aws-lambda";exportconsthandler=async(event: APIGatewayProxyEvent): Promise<APIGatewayProxyResult>=>{constname=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:
importtype{SQSEvent,SQSBatchResponse}from"aws-lambda";exportconsthandler=async(event: SQSEvent): Promise<SQSBatchResponse>=>{constfailures=[];for(constrecordofevent.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):
exportasyncfunction*handler(){yield"chunk one\n";yield"chunk two\n";yield"chunk three\n";}ReadableStream:
exportconsthandler=async()=>{constencoder=newTextEncoder();const{ readable, writable }=newTransformStream();constwriter=writable.getWriter();(async()=>{awaitwriter.write(encoder.encode("chunk one\n"));awaitwriter.write(encoder.encode("chunk two\n"));awaitwriter.close();})();returnreadable;};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";constfetch=async(request: Request): Promise<Response>=>{returnnewResponse("Hello from Bun!");};exportconsthandler=asHttpV2Handler(fetch);exportdefault{ 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";constfetch=async(request: Request): Promise<Response>=>{consturl=newURL(request.url);returnResponse.json({path: url.pathname});};exportconsthandler=asHttpV1Handler(fetch);exportdefault{ fetch };import{asResponseStreamHandler}from"@beesolve/lambda-fetch-api";exportconsthandler=asResponseStreamHandler(async()=>{conststream=newReadableStream({asyncstart(controller){controller.enqueue("chunk one\n");controller.enqueue("chunk two\n");controller.close();},});returnnewResponse(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";exportconsthandler=asHttpV2Handler(async(request)=>{constevent=getAwsV2Event();// APIGatewayProxyEventV2constcontext=getAwsContext();// Contextconsole.log(event.requestContext.requestId);console.log(context.getRemainingTimeInMillis());returnnewResponse("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*asvfrom"valibot";constAuthSchema=v.object({userId: v.string(),role: v.string()});exportconsthandler=asLambdaAuthorizedHttpV2Handler(async(request)=>{constauth=awaitgetAwsLambdaAuthorizerContext(AuthSchema);returnResponse.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";constevent=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):
exportdefault{fetch: async(request: Request): Promise<Response>=>{consturl=newURL(request.url);returnnewResponse(`Path: ${url.pathname}`);},};After (raw event style):
importtype{APIGatewayProxyEventV2}from"aws-lambda";exportconsthandler=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):
exportdefault{fetch: async(request: Request): Promise<Response>=>{returnnewResponse("Hello!");},};After (works with new runtime via adapter):
import{asHttpV2Handler}from"@beesolve/lambda-fetch-api";constfetch=async(request: Request): Promise<Response>=>{returnnewResponse("Hello!");};exportconsthandler=asHttpV2Handler(fetch);// Keep for local development with `bun run --serve`exportdefault{ 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