aws/lambda

AWS Lambda custom-runtime support.

BEAM has no AWS-managed Lambda runtime, so a Gleam function deployed to Lambda has to implement the Lambda Runtime API itself: a small HTTP contract spoken over the loopback endpoint Lambda advertises in AWS_LAMBDA_RUNTIME_API. This module is that implementation.

The lifecycle is a loop:

  1. GET /runtime/invocation/next — long-poll for the next event. The response body is the raw event payload and the response headers carry the per-invocation Context.
  2. Run the handler against the payload + context.
  3. POST /runtime/invocation/{id}/response on success, or POST /runtime/invocation/{id}/error on failure.
  4. Repeat.

Write main as:

import aws/lambda
import gleam/bit_array

pub fn main() {
  lambda.start(fn(payload, _context) {
    let assert Ok(text) = bit_array.to_string(payload)
    Ok(bit_array.from_string("hello, " <> text))
  })
}

For typed events and JSON responses use start_json together with the envelopes in aws/lambda/event and the responses in aws/lambda/response.

start blocks forever, so it only returns when the runtime cannot continue — either because the process is not running under a Lambda runtime (NotRunningInLambda) or because the Runtime API became unreachable. The returned RuntimeError says which.

A handler that raises (a panic, a failed let assert, or any Erlang exception) does not take the process down: the runtime traps it, reports it to /error as an "Unhandled" failure, and serves the next event.

Deploying

Lambda ships no managed BEAM runtime, so deploy as an OS-only custom runtime (provided.al2023). The package needs an executable bootstrap at its root that boots the VM and runs your main; for an Erlang release built against Amazon Linux 2023 that is roughly:

#!/bin/sh
set -eu
exec "${LAMBDA_TASK_ROOT}/bin/my_app" foreground

In that process Lambda sets AWS_LAMBDA_RUNTIME_API (the endpoint this module talks to), plus _HANDLER and LAMBDA_TASK_ROOT. The handler is whatever your main passes to start, so _HANDLER is unused.

Types

Low-level Runtime API client: the HTTP sender plus the AWS_LAMBDA_RUNTIME_API endpoint (a bare host:port, no scheme). Build one with api_from_env, or by hand for testing.

pub type Api {
  Api(
    send: fn(request.Request(BitArray)) -> Result(
      response.Response(BitArray),
      http_send.HttpError,
    ),
    endpoint: String,
  )
}

Constructors

Per-invocation context, populated from the Lambda-Runtime-* response headers of the next-invocation call.

pub type Context {
  Context(
    request_id: String,
    deadline_ms: Int,
    invoked_function_arn: String,
    trace_id: option.Option(String),
    client_context: option.Option(String),
    cognito_identity: option.Option(String),
  )
}

Constructors

  • Context(
      request_id: String,
      deadline_ms: Int,
      invoked_function_arn: String,
      trace_id: option.Option(String),
      client_context: option.Option(String),
      cognito_identity: option.Option(String),
    )

    Arguments

    request_id

    Lambda-Runtime-Aws-Request-Id: identifies this invocation. Echoed back in the /response and /error URLs.

    deadline_ms

    Lambda-Runtime-Deadline-Ms: the wall-clock time the invocation times out, in Unix epoch milliseconds. 0 if the header was absent.

    invoked_function_arn

    Lambda-Runtime-Invoked-Function-Arn: the ARN of the function, version, or alias the caller invoked.

    trace_id

    Lambda-Runtime-Trace-Id: the X-Ray tracing header. The runtime copies this into the _X_AMZ_TRACE_ID environment variable before calling the handler so AWS SDK calls join the active trace.

    client_context

    Lambda-Runtime-Client-Context: base64 client context, set only for invocations from the AWS Mobile SDK.

    cognito_identity

    Lambda-Runtime-Cognito-Identity: Cognito identity, set only for invocations from the AWS Mobile SDK.

A raw handler: raw event bytes and context in, response bytes or an InvocationError out.

pub type Handler =
  fn(BitArray, Context) -> Result(BitArray, InvocationError)

What rescue_call reports when a handler raises rather than returning.

pub type HandlerCrash {
  HandlerCrash(
    class: String,
    message: String,
    stack_trace: List(String),
  )
}

Constructors

  • HandlerCrash(
      class: String,
      message: String,
      stack_trace: List(String),
    )

The result of polling /runtime/invocation/next: the raw event payload plus its context. The payload is bytes — Lambda delivers every trigger (SQS, API Gateway, EventBridge, S3, …) as a JSON document, but the runtime does not assume UTF-8 so binary custom payloads pass through.

pub type Invocation {
  Invocation(context: Context, payload: BitArray)
}

Constructors

  • Invocation(context: Context, payload: BitArray)

A failure to report back to Lambda for a single invocation. Serialised to the { "errorType", "errorMessage", "stackTrace" } body the Runtime API expects, and error_type is also sent in the Lambda-Runtime-Function-Error-Type header.

pub type InvocationError {
  InvocationError(
    error_type: String,
    error_message: String,
    stack_trace: List(String),
  )
}

Constructors

  • InvocationError(
      error_type: String,
      error_message: String,
      stack_trace: List(String),
    )

A fatal error in the runtime loop itself — distinct from an InvocationError, which concerns one event. These stop serve/start.

pub type RuntimeError {
  NotRunningInLambda
  InvalidEndpoint(endpoint: String)
  Transport(http_send.HttpError)
  MissingRequestId
  UnexpectedStatus(endpoint: String, status: Int)
}

Constructors

  • NotRunningInLambda

    AWS_LAMBDA_RUNTIME_API was not set: the process is not running under a Lambda execution environment.

  • InvalidEndpoint(endpoint: String)

    AWS_LAMBDA_RUNTIME_API did not form a valid URL.

  • Transport(http_send.HttpError)

    The HTTP transport failed talking to the Runtime API.

  • MissingRequestId

    A next-invocation response arrived without the required Lambda-Runtime-Aws-Request-Id header.

  • UnexpectedStatus(endpoint: String, status: Int)

    The Runtime API answered with an unexpected status code. endpoint names the call (“next”, “response”, “error”, “init/error”).

Values

pub fn api_from_env() -> Result(Api, RuntimeError)

Build an Api from AWS_LAMBDA_RUNTIME_API. The sender is tuned for the long-poll on /next: Lambda may freeze the process between events and hold the connection open up to the 15-minute function ceiling.

pub fn get_env(name: String) -> Result(String, Nil)

Read an OS environment variable, returning Error(Nil) if it is unset. Handy inside a handler for the function’s configured environment — bucket names, table names, feature flags — without hand-rolling an FFI shim. (os:getenv/1 works in charlists; this bridges to/from Gleam String and maps the unset miss to Error(Nil).)

pub fn invocation_error(
  error_type: String,
  message: String,
) -> InvocationError

Build an InvocationError with an empty stack trace.

pub fn json_handler(
  decoder: decode.Decoder(event),
  handler: fn(event, Context) -> Result(response, String),
  encode: fn(response) -> json.Json,
) -> fn(BitArray, Context) -> Result(BitArray, InvocationError)

Adapt a typed JSON handler into a raw Handler. Useful when you want to drive the loop yourself via serve but still want the decode/encode plumbing.

pub fn next(api: Api) -> Result(Invocation, RuntimeError)

Poll GET /runtime/invocation/next for the next event. Blocks until Lambda has an invocation to deliver.

pub fn process_invocation(
  api: Api,
  set_trace_id: fn(String) -> Nil,
  handler: fn(BitArray, Context) -> Result(
    BitArray,
    InvocationError,
  ),
) -> Result(Nil, RuntimeError)

One turn of the loop: poll /next, propagate the trace id, run the handler (trapping exceptions), then post the response or the error. Ok(Nil) means the invocation was fully handled — including the case where the handler failed but the /error post succeeded. Error is reserved for Runtime API failures.

pub fn send_error(
  api: Api,
  request_id: String,
  error: InvocationError,
) -> Result(Nil, RuntimeError)

POST /runtime/invocation/{request_id}/error with the JSON error body and the Lambda-Runtime-Function-Error-Type header.

pub fn send_init_error(
  api: Api,
  error: InvocationError,
) -> Result(Nil, RuntimeError)

POST /runtime/init/error to report a fatal initialization failure before the first invocation is polled.

pub fn send_response(
  api: Api,
  request_id: String,
  body: BitArray,
) -> Result(Nil, RuntimeError)

POST /runtime/invocation/{request_id}/response with the result bytes.

pub fn serve(
  api: Api,
  set_trace_id: fn(String) -> Nil,
  handler: fn(BitArray, Context) -> Result(
    BitArray,
    InvocationError,
  ),
) -> RuntimeError

Process invocations forever. Returns the first RuntimeError that stops the loop (Runtime API unreachable, protocol violation, …). A handler that errors or raises does not stop the loop — that failure is reported to /error and the loop continues.

set_trace_id is called with the X-Ray trace id before each handler invocation; start wires it to set _X_AMZ_TRACE_ID.

pub fn start(
  handler: fn(BitArray, Context) -> Result(
    BitArray,
    InvocationError,
  ),
) -> RuntimeError

Run the Lambda runtime loop with a raw bytes handler. Reads AWS_LAMBDA_RUNTIME_API, then polls/handles/responds forever. Returns only on a fatal RuntimeError.

pub fn start_json(
  decoder: decode.Decoder(event),
  handler: fn(event, Context) -> Result(response, String),
  encode: fn(response) -> json.Json,
) -> RuntimeError

Run the loop with a typed JSON handler. The event payload is decoded with decoder; the handler’s Ok value is encoded with encode and posted as the response; the handler’s Error string is reported to Lambda. A payload that fails to decode is reported as a Runtime.InvalidEvent error without invoking the handler.

import aws/lambda
import aws/lambda/event
import aws/lambda/response

pub fn main() {
  lambda.start_json(
    event.api_gateway_v2_decoder(),
    fn(req, _ctx) { Ok(response.proxy_response(200, "hi " <> req.raw_path)) },
    response.proxy_to_json,
  )
}
Search Document