Skip to content

Repository files navigation

Lambda bun runtime

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

Installation

npm i @beesolve/lambda-bun-runtime

Quick Start

Two 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.

Handler Signature

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.

HTTP API v2 (Function URL or HTTP API Gateway)

Install @types/aws-lambda for typed event and result shapes:

npm i -D @types/aws-lambda
importtype{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",};

REST API (v1)

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}!`}),};};

Other triggers (SQS, S3, EventBridge, …)

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};};

Response Streaming

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.

Fetch API Support

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.

Installation

npm i @beesolve/lambda-fetch-api

HTTP API v2 / Function URL

The 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.

REST API (v1)

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 };

Response streaming

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"}});});

Accessing the raw AWS event and context

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");});
GetterReturns
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.

Authorizer-aware handlers

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.

Type guards

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 Support

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.

Migration Guide

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.

What changed

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.

Option 1: Use raw events (recommended for new code)

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}`,};};

Option 2: Keep Fetch API with the adapter (recommended for existing projects)

If you have existing Fetch-based handlers and want to keep them, install the companion package and wrap them:

npm i @beesolve/lambda-fetch-api

Before (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).

Summary of changes

AspectOld runtimeNew runtime
Handler signaturefetch(request: Request): Responsehandler(event, context): any
Event formatConverted to Fetch RequestRaw Lambda event (e.g., APIGatewayProxyEventV2)
Response formatFetch ResponseLambda response object (e.g., { statusCode, body })
Fetch API supportBuilt-inVia @beesolve/lambda-fetch-api adapter
WebSocket supportBuilt-in (via Bun.serve)Not applicable (use API Gateway WebSocket APIs)
Local developmentbun run --serve with same fileSame file works with dual-export pattern

Why Raw Events?

The previous approach (converting Lambda events to Fetch API objects) introduced complexity and overhead:

  1. Conversion cost — every invocation paid the price of constructing a Request and parsing a Response, even for non-HTTP triggers (SQS, S3, EventBridge, etc.)
  2. Lossy abstraction — Lambda events contain metadata (request context, authorizer claims, stage variables) that doesn't map cleanly to HTTP headers
  3. Trigger lock-in — Fetch API only makes sense for HTTP triggers, but Lambda functions handle many event sources
  4. 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-api for those who want the dual-environment pattern

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages